@omerrgocmen/crewctl 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +193 -193
  2. package/orchestrator/README.md +473 -473
  3. package/orchestrator/config.default.json +96 -90
  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 +183 -183
  69. package/orchestrator/src/cli-registry.js +125 -3
  70. package/orchestrator/src/cli.js +140 -140
  71. package/orchestrator/src/doctor.js +64 -64
  72. package/orchestrator/src/engine.js +2112 -1914
  73. package/orchestrator/src/schedule.js +124 -124
  74. package/orchestrator/src/server.js +942 -817
  75. package/orchestrator/src/skill-registry.js +272 -272
  76. package/orchestrator/src/store.js +583 -478
  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 +784 -622
  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,817 +1,942 @@
1
- // server.js — saf Node http sunucusu: REST API + SSE + statik web paneli
2
- // libuv threadpool'u genislet: bir kac donuk ag/eslesmis surucudeki bloklayan fs.stat
3
- // cagrisi (Gozat klasor gezgini) varsayilan 4 thread'i tuketip mesru fs islerini ac
4
- // birakmasin. Ilk async fs kullanimindan ONCE ayarlanmali; bu yuzden en ust satirda.
5
- process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || "32";
6
- const http = require("http");
7
- const fs = require("fs");
8
- const os = require("os");
9
- const path = require("path");
10
- const url = require("url");
11
- const store = require("./store");
12
- const engine = require("./engine");
13
- const cliRegistry = require("./cli-registry");
14
- const skillRegistry = require("./skill-registry");
15
- const checkpoints = require("./checkpoints");
16
- const schedule = require("./schedule");
17
-
18
- // Guvenlik agi: tek bir stray hata (or. kopuk ag surucusu, bir istek isleyicisindeki
19
- // beklenmeyen throw) TUM sunucuyu oldurmesin. Logla, ayakta kal.
20
- process.on("uncaughtException", (e) => console.error("[uncaughtException]", (e && e.stack) || e));
21
- process.on("unhandledRejection", (e) => console.error("[unhandledRejection]", (e && e.stack) || e));
22
-
23
- const PORT = process.env.PORT || 4317;
24
- const HOST = process.env.HOST || "127.0.0.1";
25
- const WEB = path.join(store.ASSETS, "web");
26
- const HEALTH_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
27
- const HEALTH_CACHE_VERSION = 2;
28
- const CODEX_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
29
- let codexModelRefreshDue = false;
30
- let codexModelCache = { checkedAt: 0, models: [] };
31
-
32
- // OpenCode model listesini config'e yaz: prob taze (cache-disi) liste dondurdugunde saklariz
33
- // ki bir sonraki acilista `opencode models` gecici olarak yavas/basarisiz olsa bile dropdown
34
- // bos kalmasin son bilinen liste fallback olur. codexModelCache ile ayni desen.
35
- function persistOpenCodeModels(cfg, statuses) {
36
- const oc = (statuses || []).find((c) => c.id === "opencode");
37
- const fresh = oc && oc.installed && !oc.modelsFromCache && Array.isArray(oc.models) ? oc.models : [];
38
- if (!fresh.length) return false;
39
- if (JSON.stringify((cfg.openCodeModelCache || {}).models || []) === JSON.stringify(fresh)) return false;
40
- cfg.openCodeModelCache = { checkedAt: new Date().toISOString(), models: fresh };
41
- return true;
42
- }
43
-
44
- store.ensureDirs();
45
- // Acilis maliyetini dusurmek icin onceki kesfin sonucu kullanilir; ayrintili kurallar
46
- // cli-registry.discoverInstalled icinde. Onbellek gecersiz/eksikse tam yoklama yapilir.
47
- // OpenCode modelleri persist EDILMEDIGI icin son bilinen listeyi fallback olarak veririz.
48
- // cfg ayrica model onerisinin kullanicinin modelPreferences desenlerine gore uretilmesini saglar.
49
- const bootCfg = store.loadConfig();
50
- let cliStatus = cliRegistry.discoverInstalled({
51
- cache: bootCfg.cliDiscoveryCache,
52
- openCodeModels: bootCfg.openCodeModelCache?.models,
53
- cfg: bootCfg,
54
- });
55
- {
56
- const cfg = store.loadConfig();
57
- const nextDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
58
- const discoveryCacheChanged = JSON.stringify((cfg.cliDiscoveryCache || {}).results || {}) !== JSON.stringify(nextDiscoveryCache.results);
59
- if (discoveryCacheChanged) cfg.cliDiscoveryCache = nextDiscoveryCache;
60
- const savedModels = cfg.codexModelCache && typeof cfg.codexModelCache === "object" ? cfg.codexModelCache : {};
61
- const startupCount = Number(savedModels.startupCount || 0) + 1;
62
- codexModelCache = {
63
- checkedAt: Date.parse(savedModels.checkedAt || "") || 0,
64
- models: Array.isArray(savedModels.models) ? savedModels.models : [],
65
- };
66
- codexModelRefreshDue = !codexModelCache.models.length || startupCount % 10 === 0;
67
- cfg.codexModelCache = { ...savedModels, startupCount, lastStartedAt: new Date().toISOString() };
68
- const healthCacheValid = cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION;
69
- const cached = healthCacheValid ? cfg.cliHealthCache?.results : null;
70
- let staleHealthCleared = false;
71
- if (cached && typeof cached === "object") {
72
- cliStatus = cliStatus.map((cli) => cached[cli.id] ? { ...cli, health: cached[cli.id] } : cli);
73
- } else {
74
- for (const agent of Object.values(cfg.agents || {})) {
75
- if (agent.health) { delete agent.health; staleHealthCleared = true; }
76
- }
77
- }
78
- let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
79
- if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
80
- const openCodeModelsChanged = persistOpenCodeModels(cfg, cliStatus);
81
- if (changed || staleHealthCleared || discoveryCacheChanged || openCodeModelsChanged || JSON.stringify(savedModels) !== JSON.stringify(cfg.codexModelCache)) store.saveConfig(cfg);
82
- }
83
-
84
- // ---- SSE istemcileri ----
85
- const clients = new Set();
86
- function broadcast(event, data) {
87
- const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
88
- for (const res of clients) {
89
- try {
90
- res.write(payload);
91
- } catch {}
92
- }
93
- }
94
- engine.on("log", (d) => broadcast("log", d));
95
- engine.on("status", (d) => broadcast("status", d));
96
- engine.on("result", (d) => broadcast("result", d));
97
- engine.on("activity", (d) => broadcast("activity", d));
98
- engine.on("message", (d) => broadcast("message", d));
99
- engine.on("filechange", (d) => broadcast("filechange", d));
100
- engine.on("queue", () => broadcast("queue", snapshot()));
101
- engine.on("notify", (d) => { sendNotification(d); broadcast("notify", d); });
102
-
103
- // Görev bitince/başarısız olunca kullanıcının yapılandırdığı webhook'a POST atar (Slack incoming
104
- // webhook formatıyla uyumlu: {text}). Tamamen opsiyonel ve ateşle-unut; Node'un yerleşik fetch'ini
105
- // kullanır (yeni bağımlılık yok). Hata görevi asla etkilemez. Yapılandırılmamışsa sessizce atlanır.
106
- function sendNotification(outcome) {
107
- try {
108
- const cfg = store.loadConfig();
109
- const n = cfg.notify || {};
110
- const url = String(n.webhookUrl || "").trim();
111
- if (!url || !/^https?:\/\//i.test(url)) return;
112
- if (outcome.status === "failed" ? n.onFailed === false : n.onComplete === false) return;
113
- const icon = outcome.status === "failed" ? "❌" : "✅";
114
- const head = outcome.status === "failed" ? "Görev başarısız" : "Görev tamamlandı";
115
- const detail = outcome.status === "failed"
116
- ? (outcome.error ? `\nHata: ${String(outcome.error).slice(0, 300)}` : "")
117
- : `${outcome.operator ? ` · ${outcome.operator}` : ""}${outcome.rounds ? ` · ${outcome.rounds} tur` : ""}${Number.isFinite(outcome.fileCount) ? ` · ${outcome.fileCount} dosya` : ""}${outcome.verification ? ` · ✓ ${String(outcome.verification).slice(0, 120)}` : ""}`;
118
- const text = `${icon} CrewCtl ${head}: ${String(outcome.prompt || outcome.id).slice(0, 200)}${detail}`;
119
- const controller = new AbortController();
120
- const timer = setTimeout(() => controller.abort(), 8000);
121
- fetch(url, {
122
- method: "POST",
123
- headers: { "Content-Type": "application/json" },
124
- body: JSON.stringify({ text, crewctl: outcome }),
125
- signal: controller.signal,
126
- }).catch(() => {}).finally(() => clearTimeout(timer));
127
- } catch {}
128
- }
129
-
130
- function broadcastSchedules() {
131
- broadcast("schedules", store.loadConfig().schedules || []);
132
- }
133
-
134
- // Pano/kuyruk listesi yalnizca ozet alanlari gosterir; teamState (tum agent ciktilari/mesajlari),
135
- // conversation ve changes gibi agir ic alanlar liste yukunu gereksiz sisirir ve her SSE queue
136
- // yayininda serilestirilir. Detay gorunumleri tam gorevi ayri /api/tasks/:id ile ceker; burada
137
- // bu agir alanlari cikartip yayin boyutunu kuculturuz. (planPreview/delivery/summary korunur.)
138
- function liteTask(task) {
139
- const { teamState, conversation, changes, ...rest } = task;
140
- return rest;
141
- }
142
- function snapshot() {
143
- return {
144
- pending: store.listTasks("pending").map(liteTask),
145
- approval: store.listTasks("approval").map(liteTask),
146
- // Operator-chat gorevleri de done'a yazildigi icin filtreden sonra 30 gercek gorev kalsin
147
- // diye biraz genis bir pencere (80) okunur; yine de sabit ve gecmisten bagimsizdir.
148
- done: store.listRecentTasks("done", 80).filter((task) => task.kind !== "operator-chat").slice(-30).reverse().map(liteTask),
149
- failed: store.listRecentTasks("failed", 30).reverse().map(liteTask),
150
- };
151
- }
152
-
153
- function newScheduleId() {
154
- return "sch-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7);
155
- }
156
- // Zamanlamanin operatorCli'si (verilmisse) kurulu bilinen bir CLI olmali; verilmezse gorev
157
- // calisma aninda config operatorune duser (gorev olusturmayla ayni davranis).
158
- function scheduleOperatorValid(name) {
159
- if (!name) return true;
160
- return Boolean(cliRegistry.DEFINITIONS[name]) && cliStatus.some((c) => c.id === name && c.installed);
161
- }
162
-
163
- function applyHealthToConfig(results) {
164
- const cfg = store.loadConfig();
165
- let changed = false;
166
- const cachedResults = {};
167
- for (const result of results) {
168
- cachedResults[result.id] = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
169
- for (const agent of Object.values(cfg.agents || {})) {
170
- if ((agent.adapter || cliRegistry.adapterId(agent.cmd)) !== result.id) continue;
171
- const next = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
172
- if (JSON.stringify(agent.health || {}) !== JSON.stringify(next)) {
173
- agent.health = next;
174
- changed = true;
175
- }
176
- }
177
- }
178
- const nextCache = { version: HEALTH_CACHE_VERSION, checkedAt: new Date().toISOString(), results: cachedResults };
179
- if (JSON.stringify(cfg.cliHealthCache || {}) !== JSON.stringify(nextCache)) {
180
- cfg.cliHealthCache = nextCache;
181
- changed = true;
182
- }
183
- if (changed) store.saveConfig(cfg);
184
- return cfg;
185
- }
186
-
187
- let healthRunning = false;
188
- // Motor bir gorev calistirirken (or. operatör codex) ayni CLI'ye karsi canli probe acmak
189
- // codex'in ikinci exec/app-server oturumunu SIGTERM ile kesmesine yol acar. Probe'lar en iyi
190
- // cabadir; motor mesgulken canli probe atlanip son bilinen sonuc dondurulur.
191
- function engineBusy() {
192
- try { return Boolean(engine.status().current); } catch { return false; }
193
- }
194
- async function getCodexModels(force = false) {
195
- if (!force && codexModelCache.models.length && Date.now() - codexModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return codexModelCache.models;
196
- if (engineBusy()) return codexModelCache.models;
197
- const models = await cliRegistry.listCodexModels({ timeoutMs: 20000 });
198
- codexModelCache = { checkedAt: Date.now(), models };
199
- const cfg = store.loadConfig();
200
- cfg.codexModelCache = { ...(cfg.codexModelCache || {}), checkedAt: new Date().toISOString(), models };
201
- store.saveConfig(cfg);
202
- return models;
203
- }
204
- async function refreshCliHealth(force = false) {
205
- if (healthRunning) return cliStatus;
206
- const cfg = store.loadConfig();
207
- const cachedAt = Date.parse(cfg.cliHealthCache?.checkedAt || "");
208
- if (!force && cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION && Number.isFinite(cachedAt) && Date.now() - cachedAt < HEALTH_CACHE_TTL_MS) {
209
- broadcast("cli-health", cliStatus);
210
- return cliStatus;
211
- }
212
- if (engineBusy()) {
213
- broadcast("cli-health", cliStatus);
214
- return cliStatus;
215
- }
216
- healthRunning = true;
217
- cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
218
- broadcast("cli-health", cliStatus);
219
- const generation = cliRegistry.currentProbeGeneration();
220
- try {
221
- const results = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
222
- // Motor bu sirada bir goreve basladiysa probe'lar SIGKILL edilmistir; elimizdeki
223
- // "failed/timeout" sonuclari gercek degil, iptalin yan urunudur. Onbellege yazmak
224
- // CLI'yi 6 saat boyunca yanlislikla arizali gosterirdi. Durumu bilinmiyor'a cekip
225
- // (engellemeyen gecici durum) bir sonraki tetiklemede yeniden olcmeyi bekleriz.
226
- if (cliRegistry.currentProbeGeneration() !== generation) {
227
- cliStatus = cliStatus.map((cli) => ({
228
- ...cli,
229
- health: { status: "unknown", label: "Bilinmiyor", detail: "Sağlık testi, bir görev başladığı için yarıda kesildi; sonuç kaydedilmedi." },
230
- }));
231
- broadcast("cli-health", cliStatus);
232
- return cliStatus;
233
- }
234
- cliStatus = results;
235
- applyHealthToConfig(cliStatus);
236
- broadcast("cli-health", cliStatus);
237
- return cliStatus;
238
- } finally {
239
- healthRunning = false;
240
- }
241
- }
242
-
243
- function send(res, code, obj) {
244
- res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
245
- res.end(JSON.stringify(obj));
246
- }
247
- function readBody(req) {
248
- return new Promise((resolve) => {
249
- let b = "";
250
- req.on("data", (c) => (b += c));
251
- req.on("end", () => {
252
- try {
253
- resolve(b ? JSON.parse(b) : {});
254
- } catch {
255
- resolve({});
256
- }
257
- });
258
- });
259
- }
260
- const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".png": "image/png", ".ico": "image/x-icon", ".svg": "image/svg+xml", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".webmanifest": "application/manifest+json", ".json": "application/json" };
261
- function serveStatic(res, file) {
262
- const p = path.resolve(WEB, file);
263
- if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
264
- res.writeHead(404);
265
- return res.end("not found");
266
- }
267
- res.writeHead(200, { "Content-Type": MIME[path.extname(p)] || "text/plain" });
268
- fs.createReadStream(p).pipe(res);
269
- }
270
-
271
- // ---- Klasor gezgini (async + timeout) ----
272
- // ONEMLI: eskiden statSync/readdirSync kullaniliyordu. Kopuk bir AG/eslesmis surucu
273
- // (or. dead SMB share) statSync'te saniyelerce blokladigi icin TEK is parcacikli sunucu
274
- // tamamen kitleniyordu (Gozat "acilmiyor"/donuyor). Artik tum FS erisimi async ve her
275
- // cagri kisa bir timeout'a yaristirilir; donuk surucu bloklamaz, atlanir.
276
- let driveCache = { at: 0, value: [] };
277
- function statSafe(p, ms = 700) {
278
- return Promise.race([
279
- fs.promises.stat(p).catch(() => null),
280
- new Promise((resolve) => setTimeout(() => resolve(null), ms)),
281
- ]);
282
- }
283
- async function uniqExistingDirs(items) {
284
- const seen = new Set();
285
- const out = [];
286
- for (const item of items) {
287
- if (!item.path || seen.has(item.path.toLowerCase())) continue;
288
- seen.add(item.path.toLowerCase());
289
- const st = await statSafe(item.path);
290
- if (st && st.isDirectory()) out.push(item);
291
- }
292
- return out;
293
- }
294
- async function listDrives() {
295
- if (Date.now() - driveCache.at < 30000) return driveCache.value.slice();
296
- const found = new Set();
297
- for (const d of [process.env.SystemDrive, process.env.HOMEDRIVE]) {
298
- if (d) found.add(path.parse(d).root || `${d.replace(/[\\/]$/, "")}\\`);
299
- }
300
- // Tum surucu harflerini PARALEL yokla; donuk surucu 700ms sonra atlanir (bloklamaz).
301
- const results = await Promise.all(
302
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map(async (c) => {
303
- const d = c + ":\\";
304
- const st = await statSafe(d, 700);
305
- return st && st.isDirectory() ? d : null;
306
- })
307
- );
308
- for (const d of results) if (d) found.add(d);
309
- const drives = [...found].sort((a, b) => a.localeCompare(b));
310
- driveCache = { at: Date.now(), value: drives };
311
- return drives.slice();
312
- }
313
- async function explorerPlaces() {
314
- const home = os.homedir();
315
- const oneDrive = process.env.OneDrive || process.env.OneDriveConsumer || process.env.OneDriveCommercial;
316
- const candidates = [
317
- { id: "home", label: "Ana klasör", icon: "⌂", path: home },
318
- { id: "desktop", label: "Masaüstü", icon: "▣", path: path.join(home, "Desktop") },
319
- oneDrive && { id: "onedrive-desktop", label: "OneDrive Masaüstü", icon: "▣", path: path.join(oneDrive, "Desktop") },
320
- { id: "documents", label: "Belgeler", icon: "▤", path: path.join(home, "Documents") },
321
- oneDrive && { id: "onedrive-documents", label: "OneDrive Belgeler", icon: "▤", path: path.join(oneDrive, "Documents") },
322
- { id: "downloads", label: "İndirilenler", icon: "↓", path: path.join(home, "Downloads") },
323
- { id: "project", label: "Çalışma klasörü", icon: "◇", path: store.WORK_BASE },
324
- ].filter(Boolean);
325
- return uniqExistingDirs(candidates);
326
- }
327
- async function rootBrowse(message) {
328
- if (process.platform === "win32") {
329
- const drives = await listDrives();
330
- return {
331
- path: "",
332
- parent: null,
333
- dirs: drives,
334
- drives,
335
- entries: drives.map((drive) => ({ name: drive, path: drive, type: "drive", modifiedAt: null })),
336
- places: await explorerPlaces(),
337
- isRoot: true,
338
- warning: message || "",
339
- };
340
- }
341
- return browseDir("/", message);
342
- }
343
- async function browseDir(p, warning = "") {
344
- try {
345
- if (!p) return await rootBrowse(warning);
346
- const abs = path.resolve(p);
347
- const rootStat = await statSafe(abs, 2000);
348
- if (!rootStat || !rootStat.isDirectory()) throw new Error("Klasör değil");
349
- const dirents = await fs.promises.readdir(abs, { withFileTypes: true });
350
- const wanted = dirents.filter((e) => {
351
- let isDir = false;
352
- try { isDir = e.isDirectory(); } catch { isDir = false; }
353
- return isDir && !e.name.startsWith("$") && e.name !== "System Volume Information";
354
- });
355
- // mtime'lari PARALEL topla; donuk bir alt klasor 400ms sonra tarihsiz gecer.
356
- const entries = await Promise.all(
357
- wanted.map(async (e) => {
358
- const entryPath = path.join(abs, e.name);
359
- const st = await statSafe(entryPath, 400);
360
- let modifiedAt = null;
361
- if (st) { try { modifiedAt = st.mtime.toISOString(); } catch {} }
362
- return { name: e.name, path: entryPath, type: "folder", modifiedAt };
363
- })
364
- );
365
- entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
366
- const dirs = entries.map((entry) => entry.name);
367
- const isDriveRoot = /^[A-Za-z]:\\?$/.test(abs);
368
- const up = path.dirname(abs);
369
- const parent = isDriveRoot ? "" : up === abs ? null : up;
370
- const drives = process.platform === "win32" ? await listDrives() : [];
371
- return { path: abs, parent, dirs, drives, entries, places: await explorerPlaces(), warning };
372
- } catch (e) {
373
- const requested = p ? String(p) : "";
374
- return rootBrowse(requested ? `"${requested}" açılamadı; bu bilgisayar gösteriliyor.` : e.message);
375
- }
376
- }
377
-
378
- // ---- HTTP ----
379
- const server = http.createServer(async (req, res) => {
380
- const { pathname } = url.parse(req.url, true);
381
-
382
- if (pathname === "/api/events") {
383
- res.writeHead(200, {
384
- "Content-Type": "text/event-stream",
385
- "Cache-Control": "no-cache",
386
- Connection: "keep-alive",
387
- });
388
- res.write(`event: status\ndata: ${JSON.stringify(engine.status())}\n\n`);
389
- res.write(`event: queue\ndata: ${JSON.stringify(snapshot())}\n\n`);
390
- clients.add(res);
391
- req.on("close", () => clients.delete(res));
392
- return;
393
- }
394
-
395
- if (pathname.startsWith("/api/")) {
396
- try {
397
- if (pathname === "/api/state" && req.method === "GET") {
398
- const cfg = store.loadConfig();
399
- return send(res, 200, {
400
- status: engine.status(),
401
- queue: snapshot(),
402
- config: cfg,
403
- roles: store.listRoles(),
404
- skills: skillRegistry.allSkills().map((s) => ({ name: s.name, file: s.file, description: s.description, category: s.category, appliesTo: s.appliesTo, match: s.match })),
405
- cliStatus,
406
- schedules: cfg.schedules || [],
407
- platform: process.platform,
408
- workingDirAbs: path.resolve(store.WORK_BASE, cfg.workingDir || "."),
409
- });
410
- }
411
- if (pathname === "/api/security/autonomous-consent" && req.method === "POST") {
412
- const { accepted } = await readBody(req);
413
- if (accepted !== true) return send(res, 400, { error: "Acik kabul gerekli." });
414
- const cfg = store.loadConfig();
415
- cfg.autonomousConsentAcceptedAt = cfg.autonomousConsentAcceptedAt || new Date().toISOString();
416
- store.saveConfig(cfg);
417
- return send(res, 200, { accepted: true, acceptedAt: cfg.autonomousConsentAcceptedAt });
418
- }
419
- if (pathname === "/api/fs" && req.method === "GET") {
420
- return send(res, 200, await browseDir(url.parse(req.url, true).query.path));
421
- }
422
- if (pathname === "/api/tasks" && req.method === "POST") {
423
- const { prompt, targetDir, operatorCli, executionMode, freshContext } = await readBody(req);
424
- if (!prompt || !prompt.trim()) return send(res, 400, { error: "prompt gerekli" });
425
- const cfg = store.loadConfig();
426
- // Operatör, uzman agent'lardan bağımsız bir CLI'dır (claude/codex/gemini/opencode);
427
- // operator.md rolüyle ekibi yönetir. Belirtilmemiş/geçersizse kurulu bir CLI'ye geçilir.
428
- const cliEntry = (name) => cliStatus.find((c) => c.id === name);
429
- const installedCli = (name) => name && cliRegistry.DEFINITIONS[name] && cliStatus.some((c) => c.id === name && c.installed
430
- && (name !== "opencode" || c.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)));
431
- // "testing" ve "unknown" BILGI YOKLUGUDUR, ariza kaniti degildir. Eskiden yalnizca
432
- // "ready" kabul ediliyordu; bu yuzden acilistaki saglik testi suresince (olculen: 13.3 sn)
433
- // HER gorev 409 ile reddediliyor ve kullaniciya "CLI kullanilabilir degil" deniyordu.
434
- // Bu kume kasitli olarak eski davranisin UST KUMESIDIR: bugun kabul edilen hicbir
435
- // gorev reddedilemez, yalnizca iki gecici durum artik engellemiyor. Gercekten arizali
436
- // CLI'lar (auth-required/quota/failed/timeout/version-incompatible) aynen engellenir.
437
- const TRANSIENT_HEALTH = new Set(["testing", "unknown"]);
438
- const usableCli = (name) => {
439
- if (!installedCli(name)) return false;
440
- const status = cliEntry(name)?.health?.status;
441
- return !status || status === "ready" || TRANSIENT_HEALTH.has(status);
442
- };
443
- let selectedCli = operatorCli || cfg.operator?.cli;
444
- if (!installedCli(selectedCli)) {
445
- if (cliRegistry.ensureValidOperator(cfg, cliStatus)) store.saveConfig(cfg);
446
- selectedCli = cfg.operator?.cli;
447
- }
448
- if (!installedCli(selectedCli)) {
449
- return send(res, 400, { error: "Kullanılabilir operatör CLI yok. Ayarlar → Operatör bölümünden kurulu bir CLI seçin (Claude/Codex/Gemini/OpenCode) ve gerekiyorsa Yeniden Tara'ya basın." });
450
- }
451
- if (!usableCli(selectedCli)) {
452
- const health = cliEntry(selectedCli)?.health;
453
- return send(res, 409, { error: `${cliRegistry.DEFINITIONS[selectedCli].description} şu anda kullanılabilir değil: ${health?.label || "sağlık testi bekleniyor"}. ${health?.detail || "Önce model sağlık testini tamamlayın."}` });
454
- }
455
- const mode = ["auto", "fast", "balanced", "deep"].includes(executionMode) ? executionMode : "auto";
456
- const t = store.addTask(prompt.trim(), targetDir, selectedCli, mode);
457
- // --fresh esdegeri: bu gorevde proje profili (.crewctl/CONTEXT.md) yuklenmez.
458
- if (freshContext === true) { t.freshContext = true; store.saveTask("pending", t); }
459
- engine.wake();
460
- broadcast("queue", snapshot());
461
- return send(res, 200, t);
462
- }
463
- let m;
464
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/chat$/)) && req.method === "POST") {
465
- const found = store.findTask(m[1]);
466
- if (!found || found.state !== "done" || found.task.kind === "operator-chat") return send(res, 404, { error: "tamamlanmis gorev bulunamadi" });
467
- const { question } = await readBody(req);
468
- if (!question || !String(question).trim()) return send(res, 400, { error: "soru gerekli" });
469
- const chatTask = store.addChatTask(found.task, String(question).trim());
470
- if (!engine.status().running) engine.start();
471
- else engine.wake();
472
- broadcast("queue", snapshot());
473
- return send(res, 200, chatTask);
474
- }
475
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/(approve|reject)$/)) && req.method === "POST") {
476
- const found = store.findTask(m[1]);
477
- if (!found) return send(res, 404, { error: "gorev yok" });
478
- if (m[2] === "approve") {
479
- try { store.approveTask(m[1]); }
480
- catch (error) { return send(res, 409, { error: error.message }); }
481
- } else {
482
- try { store.rejectTask(m[1]); }
483
- catch (error) { return send(res, 409, { error: error.message }); }
484
- }
485
- broadcast("queue", snapshot());
486
- return send(res, 200, { ok: true });
487
- }
488
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/restore$/)) && req.method === "POST") {
489
- const found = store.findTask(m[1]);
490
- if (!found) return send(res, 404, { error: "gorev yok" });
491
- if (engine.status().current) return send(res, 409, { error: "Motor bir gorev calistirirken surum geri yuklenemez; once durdurun." });
492
- if (!found.task.checkpointId) return send(res, 400, { error: "Bu gorev icin kayitli bir surum yok." });
493
- const cfg = store.loadConfig();
494
- const result = checkpoints.restoreCheckpoint(found.task.checkpointId, { retention: cfg.versioningRetention });
495
- if (!result.ok) return send(res, 409, { error: result.error || "Surum geri yuklenemedi." });
496
- broadcast("log", { at: new Date().toISOString(), type: "log", taskId: found.task.id, level: "warn", msg: `Surum geri yuklendi (${found.task.id} oncesi): ${result.restored} dosya geri yazildi, ${result.deleted} sonradan olusan dosya silindi. Geri almayi geri almak icin redo surumu: ${result.redoId || "yok"}.` });
497
- broadcast("queue", snapshot());
498
- return send(res, 200, result);
499
- }
500
- if (pathname === "/api/checkpoints" && req.method === "GET") {
501
- const dir = url.parse(req.url, true).query.dir;
502
- return send(res, 200, checkpoints.listCheckpoints(dir ? path.resolve(store.WORK_BASE, dir) : undefined));
503
- }
504
- if (pathname === "/api/cli/discover" && req.method === "POST") {
505
- // "Yeniden Tara" kullanicinin acik talebidir: onbellegi tumden atla, her CLI'yi yokla.
506
- // OpenCode modelleri persist edilmedigi icin son bilinen listeyi fallback ver; taze
507
- // liste gelirse persistOpenCodeModels ile guncellenir. cfg ise model onerisini
508
- // kullanicinin modelPreferences desenlerine gore urettirir.
509
- const cfg = store.loadConfig();
510
- cliStatus = cliRegistry.discoverInstalled({ force: true, cfg, openCodeModels: cfg.openCodeModelCache?.models });
511
- cfg.cliDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
512
- const body = await readBody(req);
513
- if (Array.isArray(body.ignoredAdapters)) {
514
- cfg.discoveryIgnoredAdapters = [...new Set(body.ignoredAdapters.filter((id) => cliRegistry.KNOWN_CLIS.includes(id)))];
515
- }
516
- let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
517
- if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
518
- if (persistOpenCodeModels(cfg, cliStatus)) changed = true;
519
- if (changed || Array.isArray(body.ignoredAdapters)) store.saveConfig(cfg);
520
- await refreshCliHealth(true);
521
- return send(res, 200, { cliStatus, changed, config: store.loadConfig() });
522
- }
523
- if (pathname === "/api/cli/health" && req.method === "POST") {
524
- await refreshCliHealth(true);
525
- return send(res, 200, { cliStatus, config: store.loadConfig() });
526
- }
527
- if (pathname === "/api/codex/models" && req.method === "GET") {
528
- try { return send(res, 200, { models: await getCodexModels(false) }); }
529
- catch (error) { return send(res, 502, { error: `Codex modelleri alınamadı: ${error.message}` }); }
530
- }
531
- if (pathname === "/api/schedules" && req.method === "GET") {
532
- return send(res, 200, { schedules: store.loadConfig().schedules || [] });
533
- }
534
- if (pathname === "/api/schedules" && req.method === "POST") {
535
- const body = await readBody(req);
536
- let normalized;
537
- try { normalized = schedule.normalizeSchedule(body); }
538
- catch (error) { return send(res, 400, { error: error.message }); }
539
- if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
540
- normalized.id = newScheduleId();
541
- normalized.createdAt = new Date().toISOString();
542
- const next = schedule.computeNextRun(normalized);
543
- if (next) normalized.nextRunAt = next.toISOString();
544
- const cfg = store.loadConfig();
545
- cfg.schedules = [...(cfg.schedules || []), normalized];
546
- store.saveConfig(cfg);
547
- broadcastSchedules();
548
- return send(res, 200, normalized);
549
- }
550
- if ((m = pathname.match(/^\/api\/schedules\/([^/]+)$/)) && (req.method === "PUT" || req.method === "DELETE")) {
551
- const cfg = store.loadConfig();
552
- const list = cfg.schedules || [];
553
- const index = list.findIndex((s) => s.id === m[1]);
554
- if (index < 0) return send(res, 404, { error: "zamanlama yok" });
555
- if (req.method === "DELETE") {
556
- cfg.schedules = list.filter((s) => s.id !== m[1]);
557
- store.saveConfig(cfg);
558
- broadcastSchedules();
559
- return send(res, 200, { ok: true });
560
- }
561
- const body = await readBody(req);
562
- const current = list[index];
563
- // Yalnizca enabled toggle'i mi, yoksa tam guncelleme mi? Alanlari mevcutla harmanla.
564
- const merged = { ...current, ...body, id: current.id, createdAt: current.createdAt };
565
- let normalized;
566
- try { normalized = schedule.normalizeSchedule(merged); }
567
- catch (error) { return send(res, 400, { error: error.message }); }
568
- if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
569
- normalized.id = current.id;
570
- normalized.createdAt = current.createdAt;
571
- if (current.lastRunAt) normalized.lastRunAt = current.lastRunAt;
572
- if (current.lastTaskId) normalized.lastTaskId = current.lastTaskId;
573
- const next = normalized.enabled === false ? null : schedule.computeNextRun(normalized);
574
- if (next) normalized.nextRunAt = next.toISOString();
575
- else delete normalized.nextRunAt;
576
- cfg.schedules = list.map((s, i) => (i === index ? normalized : s));
577
- store.saveConfig(cfg);
578
- broadcastSchedules();
579
- return send(res, 200, normalized);
580
- }
581
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/events$/)) && req.method === "GET") {
582
- const limit = Number(url.parse(req.url, true).query.limit) || 1000;
583
- return send(res, 200, store.listRunEvents(m[1], Math.min(limit, 5000)));
584
- }
585
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "DELETE") {
586
- const found = store.findTask(m[1]);
587
- if (!found) return send(res, 404, { error: "gorev yok" });
588
- if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev silinemez" });
589
- store.removeTask(found.state, found.task.id);
590
- broadcast("queue", snapshot());
591
- return send(res, 200, { ok: true });
592
- }
593
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "PUT") {
594
- const found = store.findTask(m[1]);
595
- if (!found) return send(res, 404, { error: "gorev yok" });
596
- if (found.state !== "pending") return send(res, 409, { error: "yalnizca bekleyen gorev guncellenebilir" });
597
- if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev guncellenemez" });
598
- const body = await readBody(req);
599
- const cfg = store.loadConfig();
600
- const t = found.task;
601
- if (typeof body.prompt === "string") {
602
- if (!body.prompt.trim()) return send(res, 400, { error: "prompt bos olamaz" });
603
- t.prompt = body.prompt.trim();
604
- }
605
- if (body.executionMode && ["auto", "fast", "balanced", "deep"].includes(body.executionMode)) t.executionMode = body.executionMode;
606
- if (body.operatorCli) {
607
- if (!cliRegistry.DEFINITIONS[body.operatorCli] || !cliStatus.some((c) => c.id === body.operatorCli && c.installed)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
608
- t.operatorCli = body.operatorCli;
609
- }
610
- if (typeof body.targetDir === "string") {
611
- if (body.targetDir.trim()) t.targetDir = body.targetDir.trim();
612
- else delete t.targetDir;
613
- }
614
- // Plan yeniden uretilsin: kullanici hedefi degistirdiyse eski plan gecersizdir.
615
- delete t.teamState; delete t.planPreview; delete t.planHash; delete t.approved;
616
- store.saveTask("pending", t);
617
- broadcast("queue", snapshot());
618
- return send(res, 200, t);
619
- }
620
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "GET") {
621
- const found = store.findTask(m[1]);
622
- return found ? send(res, 200, found) : send(res, 404, { error: "gorev yok" });
623
- }
624
- if (pathname === "/api/engine" && req.method === "POST") {
625
- const { action, mode } = await readBody(req);
626
- if (mode) engine.setMode(mode);
627
- if (action === "start") {
628
- if (!store.loadConfig().autonomousConsentAcceptedAt) return send(res, 409, { error: "Otonom CLI kosullarini once onaylayin.", code: "AUTONOMOUS_CONSENT_REQUIRED" });
629
- engine.start();
630
- }
631
- if (action === "stop") engine.stop();
632
- return send(res, 200, engine.status());
633
- }
634
- if (pathname === "/api/config" && req.method === "GET") {
635
- return send(res, 200, store.loadConfig());
636
- }
637
- if (pathname === "/api/config" && req.method === "PUT") {
638
- let cfg = await readBody(req);
639
- if (!cfg.agents || typeof cfg.agents !== "object") return send(res, 400, { error: "agents nesnesi gerekli" });
640
- if (Object.keys(cfg.agents).length < 1) return send(res, 400, { error: "operatorun altinda calisacak en az bir uzman agent gerekli" });
641
- if (!cfg.operator?.cli || !cliRegistry.KNOWN_CLIS.includes(cfg.operator.cli)) return send(res, 400, { error: "operator.cli gecerli bir CLI olmali (claude/codex/gemini/opencode)" });
642
- cfg.operator.roleFile = "roles/operator.md";
643
- delete cfg.operator.agent;
644
- // Eski UI govdeleri (operator.codexSettings / operator.model) yeni cliSettings
645
- // yapisina tasinir; dogrulama tek semada yapilir.
646
- cfg = store.normalizeConfig(cfg);
647
- cliRegistry.normalizeAgentAdapters(cfg);
648
- if (cfg.cliSettings.codex.reasoningEffort && !["low", "medium", "high", "xhigh", "max", "ultra"].includes(cfg.cliSettings.codex.reasoningEffort)) return send(res, 400, { error: "cliSettings.codex.reasoningEffort gecersiz" });
649
- if (cfg.cliSettings.codex.serviceTier && !/^[A-Za-z0-9._-]{1,64}$/.test(cfg.cliSettings.codex.serviceTier)) return send(res, 400, { error: "cliSettings.codex.serviceTier gecersiz" });
650
- for (const id of ["codex", "opencode"]) {
651
- if (cfg.cliSettings[id].model != null && typeof cfg.cliSettings[id].model !== "string") return send(res, 400, { error: `cliSettings.${id}.model metin olmali` });
652
- }
653
- if (cfg.skills !== undefined) {
654
- if (typeof cfg.skills !== "object" || cfg.skills === null) return send(res, 400, { error: "skills nesne olmali" });
655
- if (cfg.skills.enabled !== undefined && (!Array.isArray(cfg.skills.enabled) || cfg.skills.enabled.some((x) => typeof x !== "string"))) return send(res, 400, { error: "skills.enabled metin dizisi olmali" });
656
- if (cfg.skills.charBudget !== undefined && (typeof cfg.skills.charBudget !== "number" || !Number.isFinite(cfg.skills.charBudget) || cfg.skills.charBudget < 200)) return send(res, 400, { error: "skills.charBudget en az 200 olmali" });
657
- if (cfg.skills.referenceCharBudget !== undefined && (typeof cfg.skills.referenceCharBudget !== "number" || !Number.isFinite(cfg.skills.referenceCharBudget) || cfg.skills.referenceCharBudget < 300)) return send(res, 400, { error: "skills.referenceCharBudget en az 300 olmali" });
658
- for (const key of ["catalogLimit", "maxSkillsPerAssignment"]) {
659
- if (cfg.skills[key] !== undefined && (!Number.isInteger(cfg.skills[key]) || cfg.skills[key] < 1)) return send(res, 400, { error: `skills.${key} pozitif tam sayi olmali` });
660
- }
661
- if (cfg.skills.autoMatch !== undefined && typeof cfg.skills.autoMatch !== "boolean") return send(res, 400, { error: "skills.autoMatch boolean olmali" });
662
- }
663
- for (const [name, agent] of Object.entries(cfg.agents)) {
664
- if (!name.trim() || !agent.cmd || typeof agent.cmd !== "string") return send(res, 400, { error: `gecersiz agent: ${name}` });
665
- if (!Array.isArray(agent.args)) return send(res, 400, { error: `${name}.args dizi olmali` });
666
- }
667
- if (cfg.notify !== undefined) {
668
- if (typeof cfg.notify !== "object" || cfg.notify === null) return send(res, 400, { error: "notify nesne olmali" });
669
- const url = String(cfg.notify.webhookUrl || "").trim();
670
- if (url && !/^https?:\/\//i.test(url)) return send(res, 400, { error: "notify.webhookUrl http(s) ile baslamali veya bos olmali" });
671
- }
672
- store.saveConfig(cfg);
673
- broadcast("status", engine.status());
674
- return send(res, 200, { ok: true });
675
- }
676
- if (pathname === "/api/skills" && req.method === "GET") {
677
- return send(res, 200, { skills: skillRegistry.allSkills().map((s) => ({ name: s.name, file: s.file, description: s.description, category: s.category, appliesTo: s.appliesTo, match: s.match })) });
678
- }
679
- if ((m = pathname.match(/^\/api\/skills\/(.+)$/))) {
680
- let file = decodeURIComponent(m[1]);
681
- if (!file.endsWith(".md")) file += ".md";
682
- if (path.basename(file) !== file) return send(res, 400, { error: "gecersiz beceri adi" });
683
- if (req.method === "GET") return send(res, 200, { file, content: skillRegistry.readRaw(file) });
684
- if (req.method === "PUT") {
685
- const { content } = await readBody(req);
686
- const validation = skillRegistry.validateSkill(file, content || "");
687
- if (!validation.ok) return send(res, 400, { error: validation.errors.join("; ") });
688
- const saved = skillRegistry.writeSkill(file, content);
689
- return send(res, 200, { ok: true, file: saved });
690
- }
691
- if (req.method === "DELETE") {
692
- // Silmeden once beceri adini coz; etkin liste dosya adini degil frontmatter `name`i tutar.
693
- const existing = skillRegistry.loadSkill(file);
694
- const names = new Set([file.replace(/\.md$/i, ""), existing?.name].filter(Boolean));
695
- skillRegistry.deleteSkill(file);
696
- const cfg = store.loadConfig();
697
- if (Array.isArray(cfg.skills?.enabled) && cfg.skills.enabled.some((name) => names.has(name))) {
698
- cfg.skills.enabled = cfg.skills.enabled.filter((name) => !names.has(name));
699
- store.saveConfig(cfg);
700
- }
701
- return send(res, 200, { ok: true });
702
- }
703
- }
704
- if ((m = pathname.match(/^\/api\/roles\/(.+)$/))) {
705
- let file = decodeURIComponent(m[1]);
706
- if (!file.endsWith(".md")) file += ".md";
707
- if (req.method === "GET") return send(res, 200, { file, content: store.readRole(file) });
708
- if (req.method === "PUT") {
709
- const { content } = await readBody(req);
710
- store.writeRole(file, content || "");
711
- return send(res, 200, { ok: true, file });
712
- }
713
- if (req.method === "DELETE") {
714
- store.deleteRole(file);
715
- return send(res, 200, { ok: true });
716
- }
717
- }
718
- return send(res, 404, { error: "bilinmeyen endpoint" });
719
- } catch (e) {
720
- return send(res, 500, { error: e.message });
721
- }
722
- }
723
-
724
- serveStatic(res, pathname === "/" ? "index.html" : pathname.slice(1));
725
- });
726
-
727
- function openBrowser(target) {
728
- if (process.env.OPEN === "0" || process.env.NO_OPEN) return;
729
- const { spawn } = require("child_process");
730
- const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
731
- const args = process.platform === "win32" ? ["/c", "start", "", target] : [target];
732
- try { spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }).unref(); } catch {}
733
- }
734
-
735
- function startupBanner() {
736
- const G = "\x1b[32m", Y = "\x1b[33m", DIM = "\x1b[90m", B = "\x1b[1m", C = "\x1b[36m", X = "\x1b[0m";
737
- const installed = cliStatus.filter((c) => c.installed);
738
- const cfg = store.loadConfig();
739
- const url = `http://${HOST}:${PORT}`;
740
- console.log(`\n ${B}${C}CrewCtl${X}`);
741
- console.log(` ${DIM}operatör-liderliğinde çok-agent orkestratörü${X}\n`);
742
- const cliLine = cliStatus.map((c) => `${c.installed ? G + "" : DIM + "○"} ${c.id}${X}`).join(" ");
743
- console.log(` CLI: ${cliLine}`);
744
- console.log(` Operatör: ${cfg.operator?.cli || DIM + "(seçilemedi)" + X} ${DIM}·${X} Uzman: ${Object.values(cfg.agents).filter((a) => a.enabled !== false).length}`);
745
- if (!installed.length) {
746
- console.log(`\n ${Y}⚠ Hiçbir CLI kurulu değil.${X} Codex / Claude / Gemini / OpenCode'dan en az birini kurun.`);
747
- console.log(` ${DIM}Ayrıntı için: npm run doctor${X}`);
748
- }
749
- console.log(`\n ${B}▶ Panel: ${C}${url}${X}`);
750
- console.log(` ${DIM}Panelde 'Başlat'a basıp bir görev gönderin. (tarayıcı otomatik açılmazsa yukarıdaki adresi açın)${X}`);
751
- // npx gecici onbellekten calisiyorsa kalici 'crewctl' komutu kurulmamistir; kullaniciya nasil
752
- // kalici hale getirecegini hatirlat (aksi halde kapatinca 'crewctl' bulunamaz).
753
- if (/[\\/]_npx[\\/]/.test(__dirname)) {
754
- console.log(` ${DIM}Kalıcı 'crewctl' komutu için: ${Y}npm i -g @omerrgocmen/crewctl${X}`);
755
- }
756
- console.log("");
757
- openBrowser(url);
758
- }
759
-
760
- // ---- Zamanlanmis gorev tik'i ----
761
- // Zamani gelen zamanlamalari kuyruga alir. Salt hesap schedule.js'te; burada yalnizca yan
762
- // etkiler var. Motor duruyorsa gorev yalnizca 'pending'e girer (manuel ekleme davranisi);
763
- // otomatik baslatma yoktur. engine.wake() yalnizca calisan dongyu erkenden uyandirir.
764
- const SCHEDULER_TICK_MS = 30000;
765
- let schedulerRunning = false;
766
- function schedulerTick() {
767
- if (schedulerRunning) return;
768
- schedulerRunning = true;
769
- try {
770
- const cfg = store.loadConfig();
771
- const now = new Date();
772
- const due = schedule.dueSchedules(cfg.schedules || [], now);
773
- if (!due.length) return;
774
- let firedAny = false;
775
- for (const item of cfg.schedules || []) {
776
- if (!due.some((d) => d.id === item.id)) continue;
777
- try {
778
- const task = store.addScheduledTask(item);
779
- item.lastRunAt = now.toISOString();
780
- item.lastTaskId = task.id;
781
- const next = item.enabled === false ? null : schedule.computeNextRun(item, now);
782
- if (next) item.nextRunAt = next.toISOString();
783
- else delete item.nextRunAt;
784
- firedAny = true;
785
- broadcast("log", { at: now.toISOString(), type: "log", taskId: task.id, level: "info", msg: `Zamanlanmis gorev kuyruga eklendi (${item.id}): ${String(item.prompt).slice(0, 80)}` });
786
- } catch (error) {
787
- console.error("Zamanlanmis gorev olusturulamadi:", error.message);
788
- }
789
- }
790
- if (firedAny) {
791
- store.saveConfig(cfg);
792
- broadcast("queue", snapshot());
793
- broadcastSchedules();
794
- engine.wake();
795
- }
796
- } catch (error) {
797
- console.error("Zamanlayici tik hatasi:", error.message);
798
- } finally {
799
- schedulerRunning = false;
800
- }
801
- }
802
- const schedulerTimer = setInterval(schedulerTick, SCHEDULER_TICK_MS);
803
- if (schedulerTimer && typeof schedulerTimer.unref === "function") schedulerTimer.unref();
804
-
805
- server.listen(PORT, HOST, startupBanner);
806
- setImmediate(() => refreshCliHealth(false).catch((err) => console.error("CLI sağlık testi başlatılamadı:", err.message)));
807
- setTimeout(schedulerTick, 5000).unref?.();
808
- setImmediate(() => {
809
- if (codexModelRefreshDue) getCodexModels(true).catch((err) => console.error("Codex model kataloğu yenilenemedi:", err.message));
810
- });
811
- server.on("error", (err) => {
812
- if (err.code === "EADDRINUSE") {
813
- console.error(`\n \x1b[31m✗ ${HOST}:${PORT} kullanımda.\x1b[0m Farklı port ile deneyin: \x1b[1mPORT=4318 npm start\x1b[0m\n`);
814
- process.exit(1);
815
- }
816
- throw err;
817
- });
1
+ // server.js — saf Node http sunucusu: REST API + SSE + statik web paneli
2
+ // libuv threadpool'u genislet: bir kac donuk ag/eslesmis surucudeki bloklayan fs.stat
3
+ // cagrisi (Gozat klasor gezgini) varsayilan 4 thread'i tuketip mesru fs islerini ac
4
+ // birakmasin. Ilk async fs kullanimindan ONCE ayarlanmali; bu yuzden en ust satirda.
5
+ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || "32";
6
+ const http = require("http");
7
+ const fs = require("fs");
8
+ const os = require("os");
9
+ const path = require("path");
10
+ const url = require("url");
11
+ const store = require("./store");
12
+ const engine = require("./engine");
13
+ const cliRegistry = require("./cli-registry");
14
+ const skillRegistry = require("./skill-registry");
15
+ const checkpoints = require("./checkpoints");
16
+ const schedule = require("./schedule");
17
+
18
+ // Guvenlik agi: tek bir stray hata (or. kopuk ag surucusu, bir istek isleyicisindeki
19
+ // beklenmeyen throw) TUM sunucuyu oldurmesin. Logla, ayakta kal.
20
+ process.on("uncaughtException", (e) => console.error("[uncaughtException]", (e && e.stack) || e));
21
+ process.on("unhandledRejection", (e) => console.error("[unhandledRejection]", (e && e.stack) || e));
22
+
23
+ const PORT = process.env.PORT || 4317;
24
+ const HOST = process.env.HOST || "127.0.0.1";
25
+ const WEB = path.join(store.ASSETS, "web");
26
+ const HEALTH_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
27
+ const HEALTH_CACHE_VERSION = 2;
28
+ const CODEX_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
29
+ let codexModelRefreshDue = false;
30
+ let codexModelCache = { checkedAt: 0, models: [] };
31
+ let claudeModelCache = { checkedAt: 0, models: [] };
32
+
33
+ // OpenCode model listesini config'e yaz: prob taze (cache-disi) liste dondurdugunde saklariz
34
+ // ki bir sonraki acilista `opencode models` gecici olarak yavas/basarisiz olsa bile dropdown
35
+ // bos kalmasin — son bilinen liste fallback olur. codexModelCache ile ayni desen.
36
+ function persistOpenCodeModels(cfg, statuses) {
37
+ const oc = (statuses || []).find((c) => c.id === "opencode");
38
+ const fresh = oc && oc.installed && !oc.modelsFromCache && Array.isArray(oc.models) ? oc.models : [];
39
+ if (!fresh.length) return false;
40
+ if (JSON.stringify((cfg.openCodeModelCache || {}).models || []) === JSON.stringify(fresh)) return false;
41
+ cfg.openCodeModelCache = { checkedAt: new Date().toISOString(), models: fresh };
42
+ return true;
43
+ }
44
+
45
+ store.ensureDirs();
46
+ // Acilis maliyetini dusurmek icin onceki kesfin sonucu kullanilir; ayrintili kurallar
47
+ // cli-registry.discoverInstalled icinde. Onbellek gecersiz/eksikse tam yoklama yapilir.
48
+ // OpenCode modelleri persist EDILMEDIGI icin son bilinen listeyi fallback olarak veririz.
49
+ // cfg ayrica model onerisinin kullanicinin modelPreferences desenlerine gore uretilmesini saglar.
50
+ const bootCfg = store.loadConfig();
51
+ let cliStatus = cliRegistry.discoverInstalled({
52
+ cache: bootCfg.cliDiscoveryCache,
53
+ openCodeModels: bootCfg.openCodeModelCache?.models,
54
+ cfg: bootCfg,
55
+ });
56
+ {
57
+ const cfg = store.loadConfig();
58
+ const nextDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
59
+ const discoveryCacheChanged = JSON.stringify((cfg.cliDiscoveryCache || {}).results || {}) !== JSON.stringify(nextDiscoveryCache.results);
60
+ if (discoveryCacheChanged) cfg.cliDiscoveryCache = nextDiscoveryCache;
61
+ const savedModels = cfg.codexModelCache && typeof cfg.codexModelCache === "object" ? cfg.codexModelCache : {};
62
+ const startupCount = Number(savedModels.startupCount || 0) + 1;
63
+ codexModelCache = {
64
+ checkedAt: Date.parse(savedModels.checkedAt || "") || 0,
65
+ models: Array.isArray(savedModels.models) ? savedModels.models : [],
66
+ };
67
+ codexModelRefreshDue = !codexModelCache.models.length || startupCount % 10 === 0;
68
+ cfg.codexModelCache = { ...savedModels, startupCount, lastStartedAt: new Date().toISOString() };
69
+ const healthCacheValid = cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION;
70
+ const cached = healthCacheValid ? cfg.cliHealthCache?.results : null;
71
+ let staleHealthCleared = false;
72
+ if (cached && typeof cached === "object") {
73
+ cliStatus = cliStatus.map((cli) => cached[cli.id] ? { ...cli, health: cached[cli.id] } : cli);
74
+ } else {
75
+ for (const agent of Object.values(cfg.agents || {})) {
76
+ if (agent.health) { delete agent.health; staleHealthCleared = true; }
77
+ }
78
+ }
79
+ let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
80
+ if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
81
+ const openCodeModelsChanged = persistOpenCodeModels(cfg, cliStatus);
82
+ if (changed || staleHealthCleared || discoveryCacheChanged || openCodeModelsChanged || JSON.stringify(savedModels) !== JSON.stringify(cfg.codexModelCache)) store.saveConfig(cfg);
83
+ }
84
+
85
+ // ---- SSE istemcileri ----
86
+ const clients = new Set();
87
+ function broadcast(event, data) {
88
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
89
+ for (const res of clients) {
90
+ try {
91
+ res.write(payload);
92
+ } catch {}
93
+ }
94
+ }
95
+ engine.on("log", (d) => broadcast("log", d));
96
+ engine.on("status", (d) => broadcast("status", d));
97
+ engine.on("result", (d) => broadcast("result", d));
98
+ engine.on("activity", (d) => broadcast("activity", d));
99
+ engine.on("message", (d) => broadcast("message", d));
100
+ engine.on("filechange", (d) => broadcast("filechange", d));
101
+ engine.on("queue", () => broadcast("queue", snapshot()));
102
+ engine.on("notify", (d) => { sendNotification(d); broadcast("notify", d); });
103
+
104
+ // Görev bitince/başarısız olunca kullanıcının yapılandırdığı webhook'a POST atar (Slack incoming
105
+ // webhook formatıyla uyumlu: {text}). Tamamen opsiyonel ve ateşle-unut; Node'un yerleşik fetch'ini
106
+ // kullanır (yeni bağımlılık yok). Hata görevi asla etkilemez. Yapılandırılmamışsa sessizce atlanır.
107
+ function sendNotification(outcome) {
108
+ try {
109
+ const cfg = store.loadConfig();
110
+ const n = cfg.notify || {};
111
+ const url = String(n.webhookUrl || "").trim();
112
+ if (!url || !/^https?:\/\//i.test(url)) return;
113
+ if (outcome.status === "failed" ? n.onFailed === false : n.onComplete === false) return;
114
+ const icon = outcome.status === "failed" ? "" : "";
115
+ const head = outcome.status === "failed" ? "Görev başarısız" : "Görev tamamlandı";
116
+ const detail = outcome.status === "failed"
117
+ ? (outcome.error ? `\nHata: ${String(outcome.error).slice(0, 300)}` : "")
118
+ : `${outcome.operator ? ` · ${outcome.operator}` : ""}${outcome.rounds ? ` · ${outcome.rounds} tur` : ""}${Number.isFinite(outcome.fileCount) ? ` · ${outcome.fileCount} dosya` : ""}${outcome.verification ? ` · ✓ ${String(outcome.verification).slice(0, 120)}` : ""}`;
119
+ const text = `${icon} CrewCtl — ${head}: ${String(outcome.prompt || outcome.id).slice(0, 200)}${detail}`;
120
+ const controller = new AbortController();
121
+ const timer = setTimeout(() => controller.abort(), 8000);
122
+ fetch(url, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({ text, crewctl: outcome }),
126
+ signal: controller.signal,
127
+ }).catch(() => {}).finally(() => clearTimeout(timer));
128
+ } catch {}
129
+ }
130
+
131
+ function broadcastSchedules() {
132
+ broadcast("schedules", store.loadConfig().schedules || []);
133
+ }
134
+
135
+ // Pano/kuyruk listesi yalnizca ozet alanlari gosterir; teamState (tum agent ciktilari/mesajlari),
136
+ // conversation ve changes gibi agir ic alanlar liste yukunu gereksiz sisirir ve her SSE queue
137
+ // yayininda serilestirilir. Detay gorunumleri tam gorevi ayri /api/tasks/:id ile ceker; burada
138
+ // bu agir alanlari cikartip yayin boyutunu kuculturuz. (planPreview/delivery/summary korunur.)
139
+ function liteTask(task) {
140
+ const { teamState, conversation, changes, ...rest } = task;
141
+ return rest;
142
+ }
143
+ function snapshot() {
144
+ return {
145
+ pending: store.listTasks("pending").map(liteTask),
146
+ approval: store.listTasks("approval").map(liteTask),
147
+ // Operator-chat gorevleri de done'a yazildigi icin filtreden sonra 30 gercek gorev kalsin
148
+ // diye biraz genis bir pencere (80) okunur; yine de sabit ve gecmisten bagimsizdir.
149
+ done: store.listRecentTasks("done", 80).filter((task) => task.kind !== "operator-chat").slice(-30).reverse().map(liteTask),
150
+ failed: store.listRecentTasks("failed", 30).reverse().map(liteTask),
151
+ };
152
+ }
153
+
154
+ function newScheduleId() {
155
+ return "sch-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7);
156
+ }
157
+ // Zamanlamanin operatorCli'si (verilmisse) kurulu bilinen bir CLI olmali; verilmezse gorev
158
+ // calisma aninda config operatorune duser (gorev olusturmayla ayni davranis).
159
+ function scheduleOperatorValid(name) {
160
+ if (!name) return true;
161
+ return Boolean(cliRegistry.DEFINITIONS[name]) && cliStatus.some((c) => c.id === name && c.installed);
162
+ }
163
+
164
+ function applyHealthToConfig(results) {
165
+ const cfg = store.loadConfig();
166
+ let changed = false;
167
+ const cachedResults = {};
168
+ for (const result of results) {
169
+ cachedResults[result.id] = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
170
+ for (const agent of Object.values(cfg.agents || {})) {
171
+ if ((agent.adapter || cliRegistry.adapterId(agent.cmd)) !== result.id) continue;
172
+ const next = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
173
+ if (JSON.stringify(agent.health || {}) !== JSON.stringify(next)) {
174
+ agent.health = next;
175
+ changed = true;
176
+ }
177
+ }
178
+ }
179
+ const nextCache = { version: HEALTH_CACHE_VERSION, checkedAt: new Date().toISOString(), results: cachedResults };
180
+ if (JSON.stringify(cfg.cliHealthCache || {}) !== JSON.stringify(nextCache)) {
181
+ cfg.cliHealthCache = nextCache;
182
+ changed = true;
183
+ }
184
+ if (changed) store.saveConfig(cfg);
185
+ return cfg;
186
+ }
187
+
188
+ let healthRunning = false;
189
+ // Motor bir gorev calistirirken (or. operatör codex) ayni CLI'ye karsi canli probe acmak
190
+ // codex'in ikinci exec/app-server oturumunu SIGTERM ile kesmesine yol acar. Probe'lar en iyi
191
+ // cabadir; motor mesgulken canli probe atlanip son bilinen sonuc dondurulur.
192
+ function engineBusy() {
193
+ try { return Boolean(engine.status().current); } catch { return false; }
194
+ }
195
+ async function getCodexModels(force = false) {
196
+ if (!force && codexModelCache.models.length && Date.now() - codexModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return codexModelCache.models;
197
+ if (engineBusy()) return codexModelCache.models;
198
+ const models = await cliRegistry.listCodexModels({ timeoutMs: 20000 });
199
+ codexModelCache = { checkedAt: Date.now(), models };
200
+ const cfg = store.loadConfig();
201
+ cfg.codexModelCache = { ...(cfg.codexModelCache || {}), checkedAt: new Date().toISOString(), models };
202
+ store.saveConfig(cfg);
203
+ return models;
204
+ }
205
+ async function getClaudeModels(force = false) {
206
+ if (!force && claudeModelCache.models.length && Date.now() - claudeModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return claudeModelCache.models;
207
+ if (engineBusy()) return claudeModelCache.models;
208
+ const models = await cliRegistry.listClaudeModels({ timeoutMs: 20000 });
209
+ claudeModelCache = { checkedAt: Date.now(), models };
210
+ return models;
211
+ }
212
+ async function refreshCliHealth(force = false) {
213
+ if (healthRunning) return cliStatus;
214
+ const cfg = store.loadConfig();
215
+ const cachedAt = Date.parse(cfg.cliHealthCache?.checkedAt || "");
216
+ if (!force && cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION && Number.isFinite(cachedAt) && Date.now() - cachedAt < HEALTH_CACHE_TTL_MS) {
217
+ broadcast("cli-health", cliStatus);
218
+ return cliStatus;
219
+ }
220
+ if (engineBusy()) {
221
+ broadcast("cli-health", cliStatus);
222
+ return cliStatus;
223
+ }
224
+ healthRunning = true;
225
+ cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
226
+ broadcast("cli-health", cliStatus);
227
+ const generation = cliRegistry.currentProbeGeneration();
228
+ try {
229
+ const results = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
230
+ // Motor bu sirada bir goreve basladiysa probe'lar SIGKILL edilmistir; elimizdeki
231
+ // "failed/timeout" sonuclari gercek degil, iptalin yan urunudur. Onbellege yazmak
232
+ // CLI'yi 6 saat boyunca yanlislikla arizali gosterirdi. Durumu bilinmiyor'a cekip
233
+ // (engellemeyen gecici durum) bir sonraki tetiklemede yeniden olcmeyi bekleriz.
234
+ if (cliRegistry.currentProbeGeneration() !== generation) {
235
+ cliStatus = cliStatus.map((cli) => ({
236
+ ...cli,
237
+ health: { status: "unknown", label: "Bilinmiyor", detail: "Sağlık testi, bir görev başladığı için yarıda kesildi; sonuç kaydedilmedi." },
238
+ }));
239
+ broadcast("cli-health", cliStatus);
240
+ return cliStatus;
241
+ }
242
+ cliStatus = results;
243
+ applyHealthToConfig(cliStatus);
244
+ broadcast("cli-health", cliStatus);
245
+ return cliStatus;
246
+ } finally {
247
+ healthRunning = false;
248
+ }
249
+ }
250
+
251
+ function send(res, code, obj) {
252
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
253
+ res.end(JSON.stringify(obj));
254
+ }
255
+ function readBody(req) {
256
+ return new Promise((resolve) => {
257
+ let b = "";
258
+ req.on("data", (c) => (b += c));
259
+ req.on("end", () => {
260
+ try {
261
+ resolve(b ? JSON.parse(b) : {});
262
+ } catch {
263
+ resolve({});
264
+ }
265
+ });
266
+ });
267
+ }
268
+ const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".png": "image/png", ".ico": "image/x-icon", ".svg": "image/svg+xml", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".webmanifest": "application/manifest+json", ".json": "application/json" };
269
+ function serveStatic(res, file) {
270
+ const p = path.resolve(WEB, file);
271
+ if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
272
+ res.writeHead(404);
273
+ return res.end("not found");
274
+ }
275
+ res.writeHead(200, { "Content-Type": MIME[path.extname(p)] || "text/plain" });
276
+ fs.createReadStream(p).pipe(res);
277
+ }
278
+
279
+ // ---- Klasor gezgini (async + timeout) ----
280
+ // ONEMLI: eskiden statSync/readdirSync kullaniliyordu. Kopuk bir AG/eslesmis surucu
281
+ // (or. dead SMB share) statSync'te saniyelerce blokladigi icin TEK is parcacikli sunucu
282
+ // tamamen kitleniyordu (Gozat "acilmiyor"/donuyor). Artik tum FS erisimi async ve her
283
+ // cagri kisa bir timeout'a yaristirilir; donuk surucu bloklamaz, atlanir.
284
+ let driveCache = { at: 0, value: [] };
285
+ function statSafe(p, ms = 700) {
286
+ return Promise.race([
287
+ fs.promises.stat(p).catch(() => null),
288
+ new Promise((resolve) => setTimeout(() => resolve(null), ms)),
289
+ ]);
290
+ }
291
+ async function uniqExistingDirs(items) {
292
+ const seen = new Set();
293
+ const out = [];
294
+ for (const item of items) {
295
+ if (!item.path || seen.has(item.path.toLowerCase())) continue;
296
+ seen.add(item.path.toLowerCase());
297
+ const st = await statSafe(item.path);
298
+ if (st && st.isDirectory()) out.push(item);
299
+ }
300
+ return out;
301
+ }
302
+ async function listDrives() {
303
+ if (Date.now() - driveCache.at < 30000) return driveCache.value.slice();
304
+ const found = new Set();
305
+ for (const d of [process.env.SystemDrive, process.env.HOMEDRIVE]) {
306
+ if (d) found.add(path.parse(d).root || `${d.replace(/[\\/]$/, "")}\\`);
307
+ }
308
+ // Tum surucu harflerini PARALEL yokla; donuk surucu 700ms sonra atlanir (bloklamaz).
309
+ const results = await Promise.all(
310
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map(async (c) => {
311
+ const d = c + ":\\";
312
+ const st = await statSafe(d, 700);
313
+ return st && st.isDirectory() ? d : null;
314
+ })
315
+ );
316
+ for (const d of results) if (d) found.add(d);
317
+ const drives = [...found].sort((a, b) => a.localeCompare(b));
318
+ driveCache = { at: Date.now(), value: drives };
319
+ return drives.slice();
320
+ }
321
+ async function explorerPlaces() {
322
+ const home = os.homedir();
323
+ const oneDrive = process.env.OneDrive || process.env.OneDriveConsumer || process.env.OneDriveCommercial;
324
+ const candidates = [
325
+ { id: "home", label: "Ana klasör", icon: "⌂", path: home },
326
+ { id: "desktop", label: "Masaüstü", icon: "▣", path: path.join(home, "Desktop") },
327
+ oneDrive && { id: "onedrive-desktop", label: "OneDrive Masaüstü", icon: "▣", path: path.join(oneDrive, "Desktop") },
328
+ { id: "documents", label: "Belgeler", icon: "▤", path: path.join(home, "Documents") },
329
+ oneDrive && { id: "onedrive-documents", label: "OneDrive Belgeler", icon: "▤", path: path.join(oneDrive, "Documents") },
330
+ { id: "downloads", label: "İndirilenler", icon: "↓", path: path.join(home, "Downloads") },
331
+ { id: "project", label: "Çalışma klasörü", icon: "◇", path: store.WORK_BASE },
332
+ ].filter(Boolean);
333
+ return uniqExistingDirs(candidates);
334
+ }
335
+ async function rootBrowse(message) {
336
+ if (process.platform === "win32") {
337
+ const drives = await listDrives();
338
+ return {
339
+ path: "",
340
+ parent: null,
341
+ dirs: drives,
342
+ drives,
343
+ entries: drives.map((drive) => ({ name: drive, path: drive, type: "drive", modifiedAt: null })),
344
+ places: await explorerPlaces(),
345
+ isRoot: true,
346
+ warning: message || "",
347
+ };
348
+ }
349
+ return browseDir("/", message);
350
+ }
351
+ async function browseDir(p, warning = "", includeFiles = false) {
352
+ try {
353
+ if (!p) return await rootBrowse(warning);
354
+ const abs = path.resolve(p);
355
+ const rootStat = await statSafe(abs, 2000);
356
+ if (!rootStat || !rootStat.isDirectory()) throw new Error("Klasör değil");
357
+ const dirents = await fs.promises.readdir(abs, { withFileTypes: true });
358
+ const folders = dirents.filter((e) => {
359
+ let isDir = false;
360
+ try { isDir = e.isDirectory(); } catch { isDir = false; }
361
+ return isDir && !e.name.startsWith("$") && e.name !== "System Volume Information";
362
+ });
363
+ // mtime'lari PARALEL topla; donuk bir alt klasor 400ms sonra tarihsiz gecer.
364
+ const folderEntries = await Promise.all(
365
+ folders.map(async (e) => {
366
+ const entryPath = path.join(abs, e.name);
367
+ const st = await statSafe(entryPath, 400);
368
+ let modifiedAt = null;
369
+ if (st) { try { modifiedAt = st.mtime.toISOString(); } catch {} }
370
+ return { name: e.name, path: entryPath, type: "folder", modifiedAt };
371
+ })
372
+ );
373
+ folderEntries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
374
+ // includeFiles: dosya secici/goruntuleyici modu. Klasor secici (varsayilan) yalnizca klasor gosterir.
375
+ let fileEntries = [];
376
+ if (includeFiles) {
377
+ const files = dirents.filter((e) => { let f = false; try { f = e.isFile(); } catch { f = false; } return f && !e.name.startsWith("$"); });
378
+ fileEntries = await Promise.all(
379
+ files.map(async (e) => {
380
+ const entryPath = path.join(abs, e.name);
381
+ const st = await statSafe(entryPath, 400);
382
+ let modifiedAt = null, size = null;
383
+ if (st) { try { modifiedAt = st.mtime.toISOString(); } catch {} try { size = st.size; } catch {} }
384
+ return { name: e.name, path: entryPath, type: "file", modifiedAt, size };
385
+ })
386
+ );
387
+ fileEntries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
388
+ }
389
+ const entries = [...folderEntries, ...fileEntries];
390
+ const dirs = folderEntries.map((entry) => entry.name);
391
+ const isDriveRoot = /^[A-Za-z]:\\?$/.test(abs);
392
+ const up = path.dirname(abs);
393
+ const parent = isDriveRoot ? "" : up === abs ? null : up;
394
+ const drives = process.platform === "win32" ? await listDrives() : [];
395
+ return { path: abs, parent, dirs, drives, entries, places: await explorerPlaces(), warning };
396
+ } catch (e) {
397
+ const requested = p ? String(p) : "";
398
+ return rootBrowse(requested ? `"${requested}" açılamadı; bu bilgisayar gösteriliyor.` : e.message);
399
+ }
400
+ }
401
+
402
+ // @-bahsi (mention) icin proje ici dosya arama: dir altinda ozyinelemeli, q ile suzulmus, sinirli.
403
+ // Agir/gurultulu klasorleri atlar; tarama ve sonuc sayisi tavanli (buyuk repolarda her tus vurusunda
404
+ // makul kalsin diye). rel = dir'e goreli POSIX yol, path = mutlak.
405
+ async function listProjectFiles(dir, q = "", limit = 40) {
406
+ const abs = path.resolve(dir);
407
+ const st = await statSafe(abs, 2000);
408
+ if (!st || !st.isDirectory()) return { files: [], truncated: false };
409
+ const query = String(q || "").toLowerCase();
410
+ const ignore = new Set(["node_modules", ".git", ".crewctl", ".hg", ".svn", "dist", "build", ".next", ".nuxt", ".cache", "coverage", "vendor", "__pycache__", ".venv", "venv"]);
411
+ const subseq = (s, qy) => { if (!qy) return true; let i = 0; for (const c of s) { if (c === qy[i]) i++; if (i === qy.length) return true; } return false; };
412
+ const out = [];
413
+ let scanned = 0, truncated = false;
414
+ const walk = async (cur, rel, depth) => {
415
+ if (out.length >= limit || scanned > 20000 || depth > 12) { if (out.length >= limit) truncated = true; return; }
416
+ let ents; try { ents = await fs.promises.readdir(cur, { withFileTypes: true }); } catch { return; }
417
+ for (const e of ents) {
418
+ if (out.length >= limit) { truncated = true; return; }
419
+ scanned++;
420
+ let isDir = false, isFile = false;
421
+ try { isDir = e.isDirectory(); } catch {} try { isFile = e.isFile(); } catch {}
422
+ const childRel = rel ? rel + "/" + e.name : e.name;
423
+ if (isDir) { if (ignore.has(e.name) || e.name.startsWith("$")) continue; await walk(path.join(cur, e.name), childRel, depth + 1); }
424
+ else if (isFile) {
425
+ const low = childRel.toLowerCase();
426
+ if (!query || low.includes(query) || subseq(e.name.toLowerCase(), query)) out.push({ name: e.name, rel: childRel, path: path.join(cur, e.name) });
427
+ }
428
+ }
429
+ };
430
+ await walk(abs, "", 0);
431
+ // Isim tam eslesme > kisa yol onceligi ile sirala.
432
+ out.sort((a, b) => (a.name.toLowerCase().startsWith(query) === b.name.toLowerCase().startsWith(query) ? a.rel.length - b.rel.length : a.name.toLowerCase().startsWith(query) ? -1 : 1));
433
+ return { files: out.slice(0, limit), truncated };
434
+ }
435
+
436
+ // ---- HTTP ----
437
+ const server = http.createServer(async (req, res) => {
438
+ const { pathname } = url.parse(req.url, true);
439
+
440
+ if (pathname === "/api/events") {
441
+ res.writeHead(200, {
442
+ "Content-Type": "text/event-stream",
443
+ "Cache-Control": "no-cache",
444
+ Connection: "keep-alive",
445
+ });
446
+ res.write(`event: status\ndata: ${JSON.stringify(engine.status())}\n\n`);
447
+ res.write(`event: queue\ndata: ${JSON.stringify(snapshot())}\n\n`);
448
+ clients.add(res);
449
+ req.on("close", () => clients.delete(res));
450
+ return;
451
+ }
452
+
453
+ if (pathname.startsWith("/api/")) {
454
+ try {
455
+ if (pathname === "/api/state" && req.method === "GET") {
456
+ const cfg = store.loadConfig();
457
+ return send(res, 200, {
458
+ status: engine.status(),
459
+ queue: snapshot(),
460
+ config: cfg,
461
+ roles: store.listRoles(),
462
+ skills: skillRegistry.allSkills().map((s) => ({ name: s.name, file: s.file, description: s.description, category: s.category, appliesTo: s.appliesTo, match: s.match })),
463
+ cliStatus,
464
+ schedules: cfg.schedules || [],
465
+ projects: cfg.projects || [],
466
+ platform: process.platform,
467
+ workingDirAbs: path.resolve(store.WORK_BASE, cfg.workingDir || "."),
468
+ });
469
+ }
470
+ if (pathname === "/api/security/autonomous-consent" && req.method === "POST") {
471
+ const { accepted } = await readBody(req);
472
+ if (accepted !== true) return send(res, 400, { error: "Acik kabul gerekli." });
473
+ const cfg = store.loadConfig();
474
+ cfg.autonomousConsentAcceptedAt = cfg.autonomousConsentAcceptedAt || new Date().toISOString();
475
+ store.saveConfig(cfg);
476
+ return send(res, 200, { accepted: true, acceptedAt: cfg.autonomousConsentAcceptedAt });
477
+ }
478
+ if (pathname === "/api/fs" && req.method === "GET") {
479
+ const q = url.parse(req.url, true).query;
480
+ return send(res, 200, await browseDir(q.path, "", q.files === "1"));
481
+ }
482
+ if (pathname === "/api/files" && req.method === "GET") {
483
+ const q = url.parse(req.url, true).query;
484
+ if (!q.dir) return send(res, 400, { error: "dir gerekli" });
485
+ return send(res, 200, await listProjectFiles(q.dir, q.q || "", Math.min(Number(q.limit) || 40, 100)));
486
+ }
487
+ if (pathname === "/api/tasks" && req.method === "POST") {
488
+ const { prompt, targetDir, operatorCli, executionMode, freshContext, attachments, projectId } = await readBody(req);
489
+ if (!prompt || !prompt.trim()) return send(res, 400, { error: "prompt gerekli" });
490
+ const cfg = store.loadConfig();
491
+ // Proje secildiyse calisma klasoru/operator/mod varsayilanlari projeden gelir. Acikca
492
+ // gonderilen targetDir/operatorCli/executionMode her zaman onceliklidir (geriye uyum).
493
+ const project = projectId ? store.getProject(projectId) : null;
494
+ if (projectId && !project) return send(res, 404, { error: "proje bulunamadi" });
495
+ const effectiveTargetDir = (typeof targetDir === "string" && targetDir.trim()) ? targetDir.trim() : (project ? project.path : targetDir);
496
+ // Operatör, uzman agent'lardan bağımsız bir CLI'dır (claude/codex/gemini/opencode);
497
+ // operator.md rolüyle ekibi yönetir. Belirtilmemiş/geçersizse kurulu bir CLI'ye geçilir.
498
+ const cliEntry = (name) => cliStatus.find((c) => c.id === name);
499
+ const installedCli = (name) => name && cliRegistry.DEFINITIONS[name] && cliStatus.some((c) => c.id === name && c.installed
500
+ && (name !== "opencode" || c.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)));
501
+ // "testing" ve "unknown" BILGI YOKLUGUDUR, ariza kaniti degildir. Eskiden yalnizca
502
+ // "ready" kabul ediliyordu; bu yuzden acilistaki saglik testi suresince (olculen: 13.3 sn)
503
+ // HER gorev 409 ile reddediliyor ve kullaniciya "CLI kullanilabilir degil" deniyordu.
504
+ // Bu kume kasitli olarak eski davranisin UST KUMESIDIR: bugun kabul edilen hicbir
505
+ // gorev reddedilemez, yalnizca gecici durumlar artik engellemiyor. Gercek arizalar
506
+ // (auth-required/quota/version-incompatible) aynen engellenir.
507
+ // "timeout": acilistaki saglik probe'u makine yuk altindayken 45sn'de bitmeyince damgalanir
508
+ // ve gorev gonderimini 6 saat blokelardi (yanlis-pozitif). Gercek gorev calismasinin kendi
509
+ // timeout + operator fallback'i var; bu yuzden timeout'u da GECICI sayip engellemyoruz.
510
+ const TRANSIENT_HEALTH = new Set(["testing", "unknown", "timeout"]);
511
+ const usableCli = (name) => {
512
+ if (!installedCli(name)) return false;
513
+ const status = cliEntry(name)?.health?.status;
514
+ return !status || status === "ready" || TRANSIENT_HEALTH.has(status);
515
+ };
516
+ let selectedCli = operatorCli || project?.operatorCli || cfg.operator?.cli;
517
+ if (!installedCli(selectedCli)) {
518
+ if (cliRegistry.ensureValidOperator(cfg, cliStatus)) store.saveConfig(cfg);
519
+ selectedCli = cfg.operator?.cli;
520
+ }
521
+ if (!installedCli(selectedCli)) {
522
+ return send(res, 400, { error: "Kullanılabilir operatör CLI yok. Ayarlar → Operatör bölümünden kurulu bir CLI seçin (Claude/Codex/Gemini/OpenCode) ve gerekiyorsa Yeniden Tara'ya basın." });
523
+ }
524
+ if (!usableCli(selectedCli)) {
525
+ const health = cliEntry(selectedCli)?.health;
526
+ return send(res, 409, { error: `${cliRegistry.DEFINITIONS[selectedCli].description} şu anda kullanılabilir değil: ${health?.label || "sağlık testi bekleniyor"}. ${health?.detail || "Önce model sağlık testini tamamlayın."}` });
527
+ }
528
+ const mode = ["auto", "fast", "balanced", "deep"].includes(executionMode) ? executionMode
529
+ : (["auto", "fast", "balanced", "deep"].includes(project?.defaultMode) ? project.defaultMode : "auto");
530
+ // Ekli dosyalar iki turdur: (1) cihazdan base64 yukleme -> staging'e yazilir; (2) sunucu
531
+ // tarafi dosya secimi (projeden/klasorden) -> yalnizca srcPath tutulur, motor calisirken
532
+ // cozer (path cwd icindeyse KOPYA YOK, sadece goreli yol; disindaysa .crewctl'e kopyalanir).
533
+ // Limit: 12 dosya / base64 toplami ~24 MB. Asilirsa gorev olusur, ekler alinmaz + uyari.
534
+ let attachErr = "";
535
+ const rawAtt = Array.isArray(attachments) ? attachments.slice(0, 12) : [];
536
+ const b64Items = rawAtt.filter((a) => a && typeof a.dataBase64 === "string");
537
+ const pathItems = rawAtt.filter((a) => a && typeof a.srcPath === "string" && a.srcPath.trim());
538
+ const b64Ok = b64Items.reduce((n, a) => n + a.dataBase64.length, 0) <= 32 * 1024 * 1024;
539
+ if (!b64Ok) attachErr = "Yuklenen dosyalar cok buyuk (toplam ~24 MB siniri); ekler alinmadi.";
540
+ const t = store.addTask(prompt.trim(), effectiveTargetDir, selectedCli, mode);
541
+ // Proje baglama: gorevi projeye iliştir ve projenin son-kullanim zamanini guncelle.
542
+ if (project) { t.projectId = project.id; store.saveTask("pending", t); store.touchProject(project.id); }
543
+ // --fresh esdegeri: bu gorevde proje profili (.crewctl/CONTEXT.md) yuklenmez.
544
+ if (freshContext === true) { t.freshContext = true; store.saveTask("pending", t); }
545
+ const built = [];
546
+ if (b64Ok && b64Items.length) { for (const s of store.saveAttachments(t.id, b64Items)) built.push({ ...s, kind: "staged" }); }
547
+ for (const a of pathItems) {
548
+ const abs = path.resolve(String(a.srcPath));
549
+ let st = null; try { st = fs.statSync(abs); } catch {}
550
+ if (st && st.isFile()) built.push({ name: path.basename(abs), srcPath: abs, kind: "path" });
551
+ }
552
+ if (built.length) { t.attachments = built.slice(0, 12); store.saveTask("pending", t); }
553
+ if (attachErr) t.attachmentWarning = attachErr;
554
+ engine.wake();
555
+ broadcast("queue", snapshot());
556
+ return send(res, 200, t);
557
+ }
558
+ let m;
559
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/chat$/)) && req.method === "POST") {
560
+ const found = store.findTask(m[1]);
561
+ if (!found || found.state !== "done" || found.task.kind === "operator-chat") return send(res, 404, { error: "tamamlanmis gorev bulunamadi" });
562
+ const { question } = await readBody(req);
563
+ if (!question || !String(question).trim()) return send(res, 400, { error: "soru gerekli" });
564
+ const chatTask = store.addChatTask(found.task, String(question).trim());
565
+ if (!engine.status().running) engine.start();
566
+ else engine.wake();
567
+ broadcast("queue", snapshot());
568
+ return send(res, 200, chatTask);
569
+ }
570
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/(approve|reject)$/)) && req.method === "POST") {
571
+ const found = store.findTask(m[1]);
572
+ if (!found) return send(res, 404, { error: "gorev yok" });
573
+ if (m[2] === "approve") {
574
+ try { store.approveTask(m[1]); }
575
+ catch (error) { return send(res, 409, { error: error.message }); }
576
+ } else {
577
+ try { store.rejectTask(m[1]); }
578
+ catch (error) { return send(res, 409, { error: error.message }); }
579
+ }
580
+ broadcast("queue", snapshot());
581
+ return send(res, 200, { ok: true });
582
+ }
583
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/restore$/)) && req.method === "POST") {
584
+ const found = store.findTask(m[1]);
585
+ if (!found) return send(res, 404, { error: "gorev yok" });
586
+ if (engine.status().current) return send(res, 409, { error: "Motor bir gorev calistirirken surum geri yuklenemez; once durdurun." });
587
+ if (!found.task.checkpointId) return send(res, 400, { error: "Bu gorev icin kayitli bir surum yok." });
588
+ const cfg = store.loadConfig();
589
+ const result = checkpoints.restoreCheckpoint(found.task.checkpointId, { retention: cfg.versioningRetention });
590
+ if (!result.ok) return send(res, 409, { error: result.error || "Surum geri yuklenemedi." });
591
+ broadcast("log", { at: new Date().toISOString(), type: "log", taskId: found.task.id, level: "warn", msg: `Surum geri yuklendi (${found.task.id} oncesi): ${result.restored} dosya geri yazildi, ${result.deleted} sonradan olusan dosya silindi. Geri almayi geri almak icin redo surumu: ${result.redoId || "yok"}.` });
592
+ broadcast("queue", snapshot());
593
+ return send(res, 200, result);
594
+ }
595
+ if (pathname === "/api/checkpoints" && req.method === "GET") {
596
+ const dir = url.parse(req.url, true).query.dir;
597
+ return send(res, 200, checkpoints.listCheckpoints(dir ? path.resolve(store.WORK_BASE, dir) : undefined));
598
+ }
599
+ if (pathname === "/api/cli/discover" && req.method === "POST") {
600
+ // "Yeniden Tara" kullanicinin acik talebidir: onbellegi tumden atla, her CLI'yi yokla.
601
+ // OpenCode modelleri persist edilmedigi icin son bilinen listeyi fallback ver; taze
602
+ // liste gelirse persistOpenCodeModels ile guncellenir. cfg ise model onerisini
603
+ // kullanicinin modelPreferences desenlerine gore urettirir.
604
+ const cfg = store.loadConfig();
605
+ claudeModelCache = { checkedAt: 0, models: [] };
606
+ cliStatus = cliRegistry.discoverInstalled({ force: true, cfg, openCodeModels: cfg.openCodeModelCache?.models });
607
+ cfg.cliDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
608
+ const body = await readBody(req);
609
+ if (Array.isArray(body.ignoredAdapters)) {
610
+ cfg.discoveryIgnoredAdapters = [...new Set(body.ignoredAdapters.filter((id) => cliRegistry.KNOWN_CLIS.includes(id)))];
611
+ }
612
+ let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
613
+ if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
614
+ if (persistOpenCodeModels(cfg, cliStatus)) changed = true;
615
+ if (changed || Array.isArray(body.ignoredAdapters)) store.saveConfig(cfg);
616
+ await refreshCliHealth(true);
617
+ return send(res, 200, { cliStatus, changed, config: store.loadConfig() });
618
+ }
619
+ if (pathname === "/api/cli/health" && req.method === "POST") {
620
+ await refreshCliHealth(true);
621
+ return send(res, 200, { cliStatus, config: store.loadConfig() });
622
+ }
623
+ if (pathname === "/api/codex/models" && req.method === "GET") {
624
+ try { return send(res, 200, { models: await getCodexModels(false) }); }
625
+ catch (error) { return send(res, 502, { error: `Codex modelleri alınamadı: ${error.message}` }); }
626
+ }
627
+ if (pathname === "/api/claude/models" && req.method === "GET") {
628
+ try { return send(res, 200, { models: await getClaudeModels(false) }); }
629
+ catch (error) { return send(res, 502, { error: `Claude Code modelleri alınamadı: ${error.message}` }); }
630
+ }
631
+ if (pathname === "/api/projects" && req.method === "GET") {
632
+ return send(res, 200, { projects: store.listProjects() });
633
+ }
634
+ if (pathname === "/api/projects" && req.method === "POST") {
635
+ const body = await readBody(req);
636
+ let project;
637
+ try { project = store.addProject(body); }
638
+ catch (error) { return send(res, 400, { error: error.message }); }
639
+ broadcast("queue", snapshot());
640
+ return send(res, 200, project);
641
+ }
642
+ if ((m = pathname.match(/^\/api\/projects\/([^/]+)$/)) && (req.method === "PUT" || req.method === "DELETE")) {
643
+ if (req.method === "DELETE") {
644
+ if (!store.deleteProject(m[1])) return send(res, 404, { error: "proje yok" });
645
+ broadcast("queue", snapshot());
646
+ return send(res, 200, { ok: true });
647
+ }
648
+ const body = await readBody(req);
649
+ let updated;
650
+ try { updated = store.updateProject(m[1], body); }
651
+ catch (error) { return send(res, 400, { error: error.message }); }
652
+ if (!updated) return send(res, 404, { error: "proje yok" });
653
+ broadcast("queue", snapshot());
654
+ return send(res, 200, updated);
655
+ }
656
+ if (pathname === "/api/schedules" && req.method === "GET") {
657
+ return send(res, 200, { schedules: store.loadConfig().schedules || [] });
658
+ }
659
+ if (pathname === "/api/schedules" && req.method === "POST") {
660
+ const body = await readBody(req);
661
+ let normalized;
662
+ try { normalized = schedule.normalizeSchedule(body); }
663
+ catch (error) { return send(res, 400, { error: error.message }); }
664
+ if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
665
+ normalized.id = newScheduleId();
666
+ normalized.createdAt = new Date().toISOString();
667
+ const next = schedule.computeNextRun(normalized);
668
+ if (next) normalized.nextRunAt = next.toISOString();
669
+ const cfg = store.loadConfig();
670
+ cfg.schedules = [...(cfg.schedules || []), normalized];
671
+ store.saveConfig(cfg);
672
+ broadcastSchedules();
673
+ return send(res, 200, normalized);
674
+ }
675
+ if ((m = pathname.match(/^\/api\/schedules\/([^/]+)$/)) && (req.method === "PUT" || req.method === "DELETE")) {
676
+ const cfg = store.loadConfig();
677
+ const list = cfg.schedules || [];
678
+ const index = list.findIndex((s) => s.id === m[1]);
679
+ if (index < 0) return send(res, 404, { error: "zamanlama yok" });
680
+ if (req.method === "DELETE") {
681
+ cfg.schedules = list.filter((s) => s.id !== m[1]);
682
+ store.saveConfig(cfg);
683
+ broadcastSchedules();
684
+ return send(res, 200, { ok: true });
685
+ }
686
+ const body = await readBody(req);
687
+ const current = list[index];
688
+ // Yalnizca enabled toggle'i mi, yoksa tam guncelleme mi? Alanlari mevcutla harmanla.
689
+ const merged = { ...current, ...body, id: current.id, createdAt: current.createdAt };
690
+ let normalized;
691
+ try { normalized = schedule.normalizeSchedule(merged); }
692
+ catch (error) { return send(res, 400, { error: error.message }); }
693
+ if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
694
+ normalized.id = current.id;
695
+ normalized.createdAt = current.createdAt;
696
+ if (current.lastRunAt) normalized.lastRunAt = current.lastRunAt;
697
+ if (current.lastTaskId) normalized.lastTaskId = current.lastTaskId;
698
+ const next = normalized.enabled === false ? null : schedule.computeNextRun(normalized);
699
+ if (next) normalized.nextRunAt = next.toISOString();
700
+ else delete normalized.nextRunAt;
701
+ cfg.schedules = list.map((s, i) => (i === index ? normalized : s));
702
+ store.saveConfig(cfg);
703
+ broadcastSchedules();
704
+ return send(res, 200, normalized);
705
+ }
706
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/events$/)) && req.method === "GET") {
707
+ const limit = Number(url.parse(req.url, true).query.limit) || 1000;
708
+ return send(res, 200, store.listRunEvents(m[1], Math.min(limit, 5000)));
709
+ }
710
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "DELETE") {
711
+ const found = store.findTask(m[1]);
712
+ if (!found) return send(res, 404, { error: "gorev yok" });
713
+ if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev silinemez" });
714
+ store.removeTask(found.state, found.task.id);
715
+ broadcast("queue", snapshot());
716
+ return send(res, 200, { ok: true });
717
+ }
718
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "PUT") {
719
+ const found = store.findTask(m[1]);
720
+ if (!found) return send(res, 404, { error: "gorev yok" });
721
+ if (found.state !== "pending") return send(res, 409, { error: "yalnizca bekleyen gorev guncellenebilir" });
722
+ if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev guncellenemez" });
723
+ const body = await readBody(req);
724
+ const cfg = store.loadConfig();
725
+ const t = found.task;
726
+ if (typeof body.prompt === "string") {
727
+ if (!body.prompt.trim()) return send(res, 400, { error: "prompt bos olamaz" });
728
+ t.prompt = body.prompt.trim();
729
+ }
730
+ if (body.executionMode && ["auto", "fast", "balanced", "deep"].includes(body.executionMode)) t.executionMode = body.executionMode;
731
+ if (body.operatorCli) {
732
+ if (!cliRegistry.DEFINITIONS[body.operatorCli] || !cliStatus.some((c) => c.id === body.operatorCli && c.installed)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
733
+ t.operatorCli = body.operatorCli;
734
+ }
735
+ if (typeof body.targetDir === "string") {
736
+ if (body.targetDir.trim()) t.targetDir = body.targetDir.trim();
737
+ else delete t.targetDir;
738
+ }
739
+ // Plan yeniden uretilsin: kullanici hedefi degistirdiyse eski plan gecersizdir.
740
+ delete t.teamState; delete t.planPreview; delete t.planHash; delete t.approved;
741
+ store.saveTask("pending", t);
742
+ broadcast("queue", snapshot());
743
+ return send(res, 200, t);
744
+ }
745
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "GET") {
746
+ const found = store.findTask(m[1]);
747
+ return found ? send(res, 200, found) : send(res, 404, { error: "gorev yok" });
748
+ }
749
+ if (pathname === "/api/engine" && req.method === "POST") {
750
+ const { action, mode } = await readBody(req);
751
+ if (mode) engine.setMode(mode);
752
+ if (action === "start") {
753
+ if (!store.loadConfig().autonomousConsentAcceptedAt) return send(res, 409, { error: "Otonom CLI kosullarini once onaylayin.", code: "AUTONOMOUS_CONSENT_REQUIRED" });
754
+ engine.start();
755
+ }
756
+ if (action === "stop") engine.stop();
757
+ return send(res, 200, engine.status());
758
+ }
759
+ if (pathname === "/api/config" && req.method === "GET") {
760
+ return send(res, 200, store.loadConfig());
761
+ }
762
+ if (pathname === "/api/config" && req.method === "PUT") {
763
+ let cfg = await readBody(req);
764
+ if (!cfg.agents || typeof cfg.agents !== "object") return send(res, 400, { error: "agents nesnesi gerekli" });
765
+ if (Object.keys(cfg.agents).length < 1) return send(res, 400, { error: "operatorun altinda calisacak en az bir uzman agent gerekli" });
766
+ if (!cfg.operator?.cli || !cliRegistry.KNOWN_CLIS.includes(cfg.operator.cli)) return send(res, 400, { error: "operator.cli gecerli bir CLI olmali (claude/codex/gemini/opencode)" });
767
+ cfg.operator.roleFile = "roles/operator.md";
768
+ delete cfg.operator.agent;
769
+ // Eski UI govdeleri (operator.codexSettings / operator.model) yeni cliSettings
770
+ // yapisina tasinir; dogrulama tek semada yapilir.
771
+ cfg = store.normalizeConfig(cfg);
772
+ cliRegistry.normalizeAgentAdapters(cfg);
773
+ if (cfg.cliSettings.codex.reasoningEffort && !["low", "medium", "high", "xhigh", "max", "ultra"].includes(cfg.cliSettings.codex.reasoningEffort)) return send(res, 400, { error: "cliSettings.codex.reasoningEffort gecersiz" });
774
+ if (cfg.cliSettings.codex.serviceTier && !/^[A-Za-z0-9._-]{1,64}$/.test(cfg.cliSettings.codex.serviceTier)) return send(res, 400, { error: "cliSettings.codex.serviceTier gecersiz" });
775
+ for (const id of ["codex", "claude", "opencode"]) {
776
+ if (cfg.cliSettings[id].model != null && typeof cfg.cliSettings[id].model !== "string") return send(res, 400, { error: `cliSettings.${id}.model metin olmali` });
777
+ }
778
+ if (cfg.skills !== undefined) {
779
+ if (typeof cfg.skills !== "object" || cfg.skills === null) return send(res, 400, { error: "skills nesne olmali" });
780
+ if (cfg.skills.enabled !== undefined && (!Array.isArray(cfg.skills.enabled) || cfg.skills.enabled.some((x) => typeof x !== "string"))) return send(res, 400, { error: "skills.enabled metin dizisi olmali" });
781
+ if (cfg.skills.charBudget !== undefined && (typeof cfg.skills.charBudget !== "number" || !Number.isFinite(cfg.skills.charBudget) || cfg.skills.charBudget < 200)) return send(res, 400, { error: "skills.charBudget en az 200 olmali" });
782
+ if (cfg.skills.referenceCharBudget !== undefined && (typeof cfg.skills.referenceCharBudget !== "number" || !Number.isFinite(cfg.skills.referenceCharBudget) || cfg.skills.referenceCharBudget < 300)) return send(res, 400, { error: "skills.referenceCharBudget en az 300 olmali" });
783
+ for (const key of ["catalogLimit", "maxSkillsPerAssignment"]) {
784
+ if (cfg.skills[key] !== undefined && (!Number.isInteger(cfg.skills[key]) || cfg.skills[key] < 1)) return send(res, 400, { error: `skills.${key} pozitif tam sayi olmali` });
785
+ }
786
+ if (cfg.skills.autoMatch !== undefined && typeof cfg.skills.autoMatch !== "boolean") return send(res, 400, { error: "skills.autoMatch boolean olmali" });
787
+ }
788
+ for (const [name, agent] of Object.entries(cfg.agents)) {
789
+ if (!name.trim() || !agent.cmd || typeof agent.cmd !== "string") return send(res, 400, { error: `gecersiz agent: ${name}` });
790
+ if (!Array.isArray(agent.args)) return send(res, 400, { error: `${name}.args dizi olmali` });
791
+ }
792
+ if (cfg.notify !== undefined) {
793
+ if (typeof cfg.notify !== "object" || cfg.notify === null) return send(res, 400, { error: "notify nesne olmali" });
794
+ const url = String(cfg.notify.webhookUrl || "").trim();
795
+ if (url && !/^https?:\/\//i.test(url)) return send(res, 400, { error: "notify.webhookUrl http(s) ile baslamali veya bos olmali" });
796
+ }
797
+ store.saveConfig(cfg);
798
+ broadcast("status", engine.status());
799
+ return send(res, 200, { ok: true });
800
+ }
801
+ if (pathname === "/api/skills" && req.method === "GET") {
802
+ return send(res, 200, { skills: skillRegistry.allSkills().map((s) => ({ name: s.name, file: s.file, description: s.description, category: s.category, appliesTo: s.appliesTo, match: s.match })) });
803
+ }
804
+ if ((m = pathname.match(/^\/api\/skills\/(.+)$/))) {
805
+ let file = decodeURIComponent(m[1]);
806
+ if (!file.endsWith(".md")) file += ".md";
807
+ if (path.basename(file) !== file) return send(res, 400, { error: "gecersiz beceri adi" });
808
+ if (req.method === "GET") return send(res, 200, { file, content: skillRegistry.readRaw(file) });
809
+ if (req.method === "PUT") {
810
+ const { content } = await readBody(req);
811
+ const validation = skillRegistry.validateSkill(file, content || "");
812
+ if (!validation.ok) return send(res, 400, { error: validation.errors.join("; ") });
813
+ const saved = skillRegistry.writeSkill(file, content);
814
+ return send(res, 200, { ok: true, file: saved });
815
+ }
816
+ if (req.method === "DELETE") {
817
+ // Silmeden once beceri adini coz; etkin liste dosya adini degil frontmatter `name`i tutar.
818
+ const existing = skillRegistry.loadSkill(file);
819
+ const names = new Set([file.replace(/\.md$/i, ""), existing?.name].filter(Boolean));
820
+ skillRegistry.deleteSkill(file);
821
+ const cfg = store.loadConfig();
822
+ if (Array.isArray(cfg.skills?.enabled) && cfg.skills.enabled.some((name) => names.has(name))) {
823
+ cfg.skills.enabled = cfg.skills.enabled.filter((name) => !names.has(name));
824
+ store.saveConfig(cfg);
825
+ }
826
+ return send(res, 200, { ok: true });
827
+ }
828
+ }
829
+ if ((m = pathname.match(/^\/api\/roles\/(.+)$/))) {
830
+ let file = decodeURIComponent(m[1]);
831
+ if (!file.endsWith(".md")) file += ".md";
832
+ if (req.method === "GET") return send(res, 200, { file, content: store.readRole(file) });
833
+ if (req.method === "PUT") {
834
+ const { content } = await readBody(req);
835
+ store.writeRole(file, content || "");
836
+ return send(res, 200, { ok: true, file });
837
+ }
838
+ if (req.method === "DELETE") {
839
+ store.deleteRole(file);
840
+ return send(res, 200, { ok: true });
841
+ }
842
+ }
843
+ return send(res, 404, { error: "bilinmeyen endpoint" });
844
+ } catch (e) {
845
+ return send(res, 500, { error: e.message });
846
+ }
847
+ }
848
+
849
+ serveStatic(res, pathname === "/" ? "index.html" : pathname.slice(1));
850
+ });
851
+
852
+ function openBrowser(target) {
853
+ if (process.env.OPEN === "0" || process.env.NO_OPEN) return;
854
+ const { spawn } = require("child_process");
855
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
856
+ const args = process.platform === "win32" ? ["/c", "start", "", target] : [target];
857
+ try { spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }).unref(); } catch {}
858
+ }
859
+
860
+ function startupBanner() {
861
+ const G = "\x1b[32m", Y = "\x1b[33m", DIM = "\x1b[90m", B = "\x1b[1m", C = "\x1b[36m", X = "\x1b[0m";
862
+ const installed = cliStatus.filter((c) => c.installed);
863
+ const cfg = store.loadConfig();
864
+ const url = `http://${HOST}:${PORT}`;
865
+ console.log(`\n ${B}${C}CrewCtl${X}`);
866
+ console.log(` ${DIM}operatör-liderliğinde çok-agent orkestratörü${X}\n`);
867
+ const cliLine = cliStatus.map((c) => `${c.installed ? G + "●" : DIM + "○"} ${c.id}${X}`).join(" ");
868
+ console.log(` CLI: ${cliLine}`);
869
+ console.log(` Operatör: ${cfg.operator?.cli || DIM + "(seçilemedi)" + X} ${DIM}·${X} Uzman: ${Object.values(cfg.agents).filter((a) => a.enabled !== false).length}`);
870
+ if (!installed.length) {
871
+ console.log(`\n ${Y}⚠ Hiçbir CLI kurulu değil.${X} Codex / Claude / Gemini / OpenCode'dan en az birini kurun.`);
872
+ console.log(` ${DIM}Ayrıntı için: npm run doctor${X}`);
873
+ }
874
+ console.log(`\n ${B}▶ Panel: ${C}${url}${X}`);
875
+ console.log(` ${DIM}Panelde 'Başlat'a basıp bir görev gönderin. (tarayıcı otomatik açılmazsa yukarıdaki adresi açın)${X}`);
876
+ // npx gecici onbellekten calisiyorsa kalici 'crewctl' komutu kurulmamistir; kullaniciya nasil
877
+ // kalici hale getirecegini hatirlat (aksi halde kapatinca 'crewctl' bulunamaz).
878
+ if (/[\\/]_npx[\\/]/.test(__dirname)) {
879
+ console.log(` ${DIM}Kalıcı 'crewctl' komutu için: ${Y}npm i -g @omerrgocmen/crewctl${X}`);
880
+ }
881
+ console.log("");
882
+ openBrowser(url);
883
+ }
884
+
885
+ // ---- Zamanlanmis gorev tik'i ----
886
+ // Zamani gelen zamanlamalari kuyruga alir. Salt hesap schedule.js'te; burada yalnizca yan
887
+ // etkiler var. Motor duruyorsa gorev yalnizca 'pending'e girer (manuel ekleme davranisi);
888
+ // otomatik baslatma yoktur. engine.wake() yalnizca calisan dongyu erkenden uyandirir.
889
+ const SCHEDULER_TICK_MS = 30000;
890
+ let schedulerRunning = false;
891
+ function schedulerTick() {
892
+ if (schedulerRunning) return;
893
+ schedulerRunning = true;
894
+ try {
895
+ const cfg = store.loadConfig();
896
+ const now = new Date();
897
+ const due = schedule.dueSchedules(cfg.schedules || [], now);
898
+ if (!due.length) return;
899
+ let firedAny = false;
900
+ for (const item of cfg.schedules || []) {
901
+ if (!due.some((d) => d.id === item.id)) continue;
902
+ try {
903
+ const task = store.addScheduledTask(item);
904
+ item.lastRunAt = now.toISOString();
905
+ item.lastTaskId = task.id;
906
+ const next = item.enabled === false ? null : schedule.computeNextRun(item, now);
907
+ if (next) item.nextRunAt = next.toISOString();
908
+ else delete item.nextRunAt;
909
+ firedAny = true;
910
+ broadcast("log", { at: now.toISOString(), type: "log", taskId: task.id, level: "info", msg: `Zamanlanmis gorev kuyruga eklendi (${item.id}): ${String(item.prompt).slice(0, 80)}` });
911
+ } catch (error) {
912
+ console.error("Zamanlanmis gorev olusturulamadi:", error.message);
913
+ }
914
+ }
915
+ if (firedAny) {
916
+ store.saveConfig(cfg);
917
+ broadcast("queue", snapshot());
918
+ broadcastSchedules();
919
+ engine.wake();
920
+ }
921
+ } catch (error) {
922
+ console.error("Zamanlayici tik hatasi:", error.message);
923
+ } finally {
924
+ schedulerRunning = false;
925
+ }
926
+ }
927
+ const schedulerTimer = setInterval(schedulerTick, SCHEDULER_TICK_MS);
928
+ if (schedulerTimer && typeof schedulerTimer.unref === "function") schedulerTimer.unref();
929
+
930
+ server.listen(PORT, HOST, startupBanner);
931
+ setImmediate(() => refreshCliHealth(false).catch((err) => console.error("CLI sağlık testi başlatılamadı:", err.message)));
932
+ setTimeout(schedulerTick, 5000).unref?.();
933
+ setImmediate(() => {
934
+ if (codexModelRefreshDue) getCodexModels(true).catch((err) => console.error("Codex model kataloğu yenilenemedi:", err.message));
935
+ });
936
+ server.on("error", (err) => {
937
+ if (err.code === "EADDRINUSE") {
938
+ console.error(`\n \x1b[31m✗ ${HOST}:${PORT} kullanımda.\x1b[0m Farklı port ile deneyin: \x1b[1mPORT=4318 npm start\x1b[0m\n`);
939
+ process.exit(1);
940
+ }
941
+ throw err;
942
+ });