@omerrgocmen/crewctl 1.0.3 → 1.3.0

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 (91) hide show
  1. package/README.md +193 -193
  2. package/orchestrator/README.md +473 -462
  3. package/orchestrator/config.default.json +79 -71
  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/cli-registry.js +740 -616
  69. package/orchestrator/src/cli.js +140 -135
  70. package/orchestrator/src/doctor.js +64 -64
  71. package/orchestrator/src/engine.js +1735 -1552
  72. package/orchestrator/src/schedule.js +126 -126
  73. package/orchestrator/src/server.js +818 -766
  74. package/orchestrator/src/skill-registry.js +272 -272
  75. package/orchestrator/src/store.js +470 -399
  76. package/orchestrator/web/OrbitControls.js +1417 -1417
  77. package/orchestrator/web/board.html +146 -146
  78. package/orchestrator/web/code.html +213 -213
  79. package/orchestrator/web/flow.html +741 -741
  80. package/orchestrator/web/index.html +602 -588
  81. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -231
  82. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -104
  83. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -95
  84. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -99
  85. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -77
  86. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -415
  87. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -45
  88. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -66
  89. package/orchestrator/web/site.webmanifest +12 -12
  90. package/orchestrator/web/three.module.min.js +6 -6
  91. package/package.json +51 -51
@@ -1,766 +1,818 @@
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
- store.ensureDirs();
33
- let cliStatus = cliRegistry.discoverInstalled();
34
- {
35
- const cfg = store.loadConfig();
36
- const savedModels = cfg.codexModelCache && typeof cfg.codexModelCache === "object" ? cfg.codexModelCache : {};
37
- const startupCount = Number(savedModels.startupCount || 0) + 1;
38
- codexModelCache = {
39
- checkedAt: Date.parse(savedModels.checkedAt || "") || 0,
40
- models: Array.isArray(savedModels.models) ? savedModels.models : [],
41
- };
42
- codexModelRefreshDue = !codexModelCache.models.length || startupCount % 10 === 0;
43
- cfg.codexModelCache = { ...savedModels, startupCount, lastStartedAt: new Date().toISOString() };
44
- const healthCacheValid = cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION;
45
- const cached = healthCacheValid ? cfg.cliHealthCache?.results : null;
46
- let staleHealthCleared = false;
47
- if (cached && typeof cached === "object") {
48
- cliStatus = cliStatus.map((cli) => cached[cli.id] ? { ...cli, health: cached[cli.id] } : cli);
49
- } else {
50
- for (const agent of Object.values(cfg.agents || {})) {
51
- if (agent.health) { delete agent.health; staleHealthCleared = true; }
52
- }
53
- }
54
- let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
55
- if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
56
- if (changed || staleHealthCleared || JSON.stringify(savedModels) !== JSON.stringify(cfg.codexModelCache)) store.saveConfig(cfg);
57
- }
58
-
59
- // ---- SSE istemcileri ----
60
- const clients = new Set();
61
- function broadcast(event, data) {
62
- const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
63
- for (const res of clients) {
64
- try {
65
- res.write(payload);
66
- } catch {}
67
- }
68
- }
69
- engine.on("log", (d) => broadcast("log", d));
70
- engine.on("status", (d) => broadcast("status", d));
71
- engine.on("result", (d) => broadcast("result", d));
72
- engine.on("activity", (d) => broadcast("activity", d));
73
- engine.on("message", (d) => broadcast("message", d));
74
- engine.on("filechange", (d) => broadcast("filechange", d));
75
- engine.on("queue", () => broadcast("queue", snapshot()));
76
- engine.on("notify", (d) => { sendNotification(d); broadcast("notify", d); });
77
-
78
- // Görev bitince/başarısız olunca kullanıcının yapılandırdığı webhook'a POST atar (Slack incoming
79
- // webhook formatıyla uyumlu: {text}). Tamamen opsiyonel ve ateşle-unut; Node'un yerleşik fetch'ini
80
- // kullanır (yeni bağımlılık yok). Hata görevi asla etkilemez. Yapılandırılmamışsa sessizce atlanır.
81
- function sendNotification(outcome) {
82
- try {
83
- const cfg = store.loadConfig();
84
- const n = cfg.notify || {};
85
- const url = String(n.webhookUrl || "").trim();
86
- if (!url || !/^https?:\/\//i.test(url)) return;
87
- if (outcome.status === "failed" ? n.onFailed === false : n.onComplete === false) return;
88
- const icon = outcome.status === "failed" ? "❌" : "✅";
89
- const head = outcome.status === "failed" ? "Görev başarısız" : "Görev tamamlandı";
90
- const detail = outcome.status === "failed"
91
- ? (outcome.error ? `\nHata: ${String(outcome.error).slice(0, 300)}` : "")
92
- : `${outcome.operator ? ` · ${outcome.operator}` : ""}${outcome.rounds ? ` · ${outcome.rounds} tur` : ""}${Number.isFinite(outcome.fileCount) ? ` · ${outcome.fileCount} dosya` : ""}${outcome.verification ? ` · ✓ ${String(outcome.verification).slice(0, 120)}` : ""}`;
93
- const text = `${icon} CrewCtl — ${head}: ${String(outcome.prompt || outcome.id).slice(0, 200)}${detail}`;
94
- const controller = new AbortController();
95
- const timer = setTimeout(() => controller.abort(), 8000);
96
- fetch(url, {
97
- method: "POST",
98
- headers: { "Content-Type": "application/json" },
99
- body: JSON.stringify({ text, crewctl: outcome }),
100
- signal: controller.signal,
101
- }).catch(() => {}).finally(() => clearTimeout(timer));
102
- } catch {}
103
- }
104
-
105
- function broadcastSchedules() {
106
- broadcast("schedules", store.loadConfig().schedules || []);
107
- }
108
-
109
- // Pano/kuyruk listesi yalnizca ozet alanlari gosterir; teamState (tum agent ciktilari/mesajlari),
110
- // conversation ve changes gibi agir ic alanlar liste yukunu gereksiz sisirir ve her SSE queue
111
- // yayininda serilestirilir. Detay gorunumleri tam gorevi ayri /api/tasks/:id ile ceker; burada
112
- // bu agir alanlari cikartip yayin boyutunu kuculturuz. (planPreview/delivery/summary korunur.)
113
- function liteTask(task) {
114
- const { teamState, conversation, changes, ...rest } = task;
115
- return rest;
116
- }
117
- function snapshot() {
118
- return {
119
- pending: store.listTasks("pending").map(liteTask),
120
- approval: store.listTasks("approval").map(liteTask),
121
- // Operator-chat gorevleri de done'a yazildigi icin filtreden sonra 30 gercek gorev kalsin
122
- // diye biraz genis bir pencere (80) okunur; yine de sabit ve gecmisten bagimsizdir.
123
- done: store.listRecentTasks("done", 80).filter((task) => task.kind !== "operator-chat").slice(-30).reverse().map(liteTask),
124
- failed: store.listRecentTasks("failed", 30).reverse().map(liteTask),
125
- };
126
- }
127
-
128
- function newScheduleId() {
129
- return "sch-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7);
130
- }
131
- // Zamanlamanin operatorCli'si (verilmisse) kurulu bilinen bir CLI olmali; verilmezse gorev
132
- // calisma aninda config operatorune duser (gorev olusturmayla ayni davranis).
133
- function scheduleOperatorValid(name) {
134
- if (!name) return true;
135
- return Boolean(cliRegistry.DEFINITIONS[name]) && cliStatus.some((c) => c.id === name && c.installed);
136
- }
137
-
138
- function applyHealthToConfig(results) {
139
- const cfg = store.loadConfig();
140
- let changed = false;
141
- const cachedResults = {};
142
- for (const result of results) {
143
- cachedResults[result.id] = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
144
- for (const agent of Object.values(cfg.agents || {})) {
145
- if ((agent.adapter || cliRegistry.adapterId(agent.cmd)) !== result.id) continue;
146
- const next = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
147
- if (JSON.stringify(agent.health || {}) !== JSON.stringify(next)) {
148
- agent.health = next;
149
- changed = true;
150
- }
151
- }
152
- }
153
- const nextCache = { version: HEALTH_CACHE_VERSION, checkedAt: new Date().toISOString(), results: cachedResults };
154
- if (JSON.stringify(cfg.cliHealthCache || {}) !== JSON.stringify(nextCache)) {
155
- cfg.cliHealthCache = nextCache;
156
- changed = true;
157
- }
158
- if (changed) store.saveConfig(cfg);
159
- return cfg;
160
- }
161
-
162
- let healthRunning = false;
163
- // Motor bir gorev calistirirken (or. operatör codex) ayni CLI'ye karsi canli probe acmak
164
- // codex'in ikinci exec/app-server oturumunu SIGTERM ile kesmesine yol acar. Probe'lar en iyi
165
- // cabadir; motor mesgulken canli probe atlanip son bilinen sonuc dondurulur.
166
- function engineBusy() {
167
- try { return Boolean(engine.status().current); } catch { return false; }
168
- }
169
- async function getCodexModels(force = false) {
170
- if (!force && codexModelCache.models.length && Date.now() - codexModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return codexModelCache.models;
171
- if (engineBusy()) return codexModelCache.models;
172
- const models = await cliRegistry.listCodexModels({ timeoutMs: 20000 });
173
- codexModelCache = { checkedAt: Date.now(), models };
174
- const cfg = store.loadConfig();
175
- cfg.codexModelCache = { ...(cfg.codexModelCache || {}), checkedAt: new Date().toISOString(), models };
176
- store.saveConfig(cfg);
177
- return models;
178
- }
179
- async function refreshCliHealth(force = false) {
180
- if (healthRunning) return cliStatus;
181
- const cfg = store.loadConfig();
182
- const cachedAt = Date.parse(cfg.cliHealthCache?.checkedAt || "");
183
- if (!force && cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION && Number.isFinite(cachedAt) && Date.now() - cachedAt < HEALTH_CACHE_TTL_MS) {
184
- broadcast("cli-health", cliStatus);
185
- return cliStatus;
186
- }
187
- if (engineBusy()) {
188
- broadcast("cli-health", cliStatus);
189
- return cliStatus;
190
- }
191
- healthRunning = true;
192
- cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
193
- broadcast("cli-health", cliStatus);
194
- try {
195
- cliStatus = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
196
- applyHealthToConfig(cliStatus);
197
- broadcast("cli-health", cliStatus);
198
- return cliStatus;
199
- } finally {
200
- healthRunning = false;
201
- }
202
- }
203
-
204
- // ---- yardimcilar ----
205
- function send(res, code, obj) {
206
- res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
207
- res.end(JSON.stringify(obj));
208
- }
209
- function readBody(req) {
210
- return new Promise((resolve) => {
211
- let b = "";
212
- req.on("data", (c) => (b += c));
213
- req.on("end", () => {
214
- try {
215
- resolve(b ? JSON.parse(b) : {});
216
- } catch {
217
- resolve({});
218
- }
219
- });
220
- });
221
- }
222
- 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" };
223
- function serveStatic(res, file) {
224
- const p = path.resolve(WEB, file);
225
- if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
226
- res.writeHead(404);
227
- return res.end("not found");
228
- }
229
- res.writeHead(200, { "Content-Type": MIME[path.extname(p)] || "text/plain" });
230
- fs.createReadStream(p).pipe(res);
231
- }
232
-
233
- // ---- Klasor gezgini (async + timeout) ----
234
- // ONEMLI: eskiden statSync/readdirSync kullaniliyordu. Kopuk bir AG/eslesmis surucu
235
- // (or. dead SMB share) statSync'te saniyelerce blokladigi icin TEK is parcacikli sunucu
236
- // tamamen kitleniyordu (Gozat "acilmiyor"/donuyor). Artik tum FS erisimi async ve her
237
- // cagri kisa bir timeout'a yaristirilir; donuk surucu bloklamaz, atlanir.
238
- let driveCache = { at: 0, value: [] };
239
- function statSafe(p, ms = 700) {
240
- return Promise.race([
241
- fs.promises.stat(p).catch(() => null),
242
- new Promise((resolve) => setTimeout(() => resolve(null), ms)),
243
- ]);
244
- }
245
- async function uniqExistingDirs(items) {
246
- const seen = new Set();
247
- const out = [];
248
- for (const item of items) {
249
- if (!item.path || seen.has(item.path.toLowerCase())) continue;
250
- seen.add(item.path.toLowerCase());
251
- const st = await statSafe(item.path);
252
- if (st && st.isDirectory()) out.push(item);
253
- }
254
- return out;
255
- }
256
- async function listDrives() {
257
- if (Date.now() - driveCache.at < 30000) return driveCache.value.slice();
258
- const found = new Set();
259
- for (const d of [process.env.SystemDrive, process.env.HOMEDRIVE]) {
260
- if (d) found.add(path.parse(d).root || `${d.replace(/[\\/]$/, "")}\\`);
261
- }
262
- // Tum surucu harflerini PARALEL yokla; donuk surucu 700ms sonra atlanir (bloklamaz).
263
- const results = await Promise.all(
264
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map(async (c) => {
265
- const d = c + ":\\";
266
- const st = await statSafe(d, 700);
267
- return st && st.isDirectory() ? d : null;
268
- })
269
- );
270
- for (const d of results) if (d) found.add(d);
271
- const drives = [...found].sort((a, b) => a.localeCompare(b));
272
- driveCache = { at: Date.now(), value: drives };
273
- return drives.slice();
274
- }
275
- async function explorerPlaces() {
276
- const home = os.homedir();
277
- const oneDrive = process.env.OneDrive || process.env.OneDriveConsumer || process.env.OneDriveCommercial;
278
- const candidates = [
279
- { id: "home", label: "Ana klasör", icon: "⌂", path: home },
280
- { id: "desktop", label: "Masaüstü", icon: "▣", path: path.join(home, "Desktop") },
281
- oneDrive && { id: "onedrive-desktop", label: "OneDrive Masaüstü", icon: "▣", path: path.join(oneDrive, "Desktop") },
282
- { id: "documents", label: "Belgeler", icon: "▤", path: path.join(home, "Documents") },
283
- oneDrive && { id: "onedrive-documents", label: "OneDrive Belgeler", icon: "▤", path: path.join(oneDrive, "Documents") },
284
- { id: "downloads", label: "İndirilenler", icon: "↓", path: path.join(home, "Downloads") },
285
- { id: "project", label: "Çalışma klasörü", icon: "◇", path: store.WORK_BASE },
286
- ].filter(Boolean);
287
- return uniqExistingDirs(candidates);
288
- }
289
- async function rootBrowse(message) {
290
- if (process.platform === "win32") {
291
- const drives = await listDrives();
292
- return {
293
- path: "",
294
- parent: null,
295
- dirs: drives,
296
- drives,
297
- entries: drives.map((drive) => ({ name: drive, path: drive, type: "drive", modifiedAt: null })),
298
- places: await explorerPlaces(),
299
- isRoot: true,
300
- warning: message || "",
301
- };
302
- }
303
- return browseDir("/", message);
304
- }
305
- async function browseDir(p, warning = "") {
306
- try {
307
- if (!p) return await rootBrowse(warning);
308
- const abs = path.resolve(p);
309
- const rootStat = await statSafe(abs, 2000);
310
- if (!rootStat || !rootStat.isDirectory()) throw new Error("Klasör değil");
311
- const dirents = await fs.promises.readdir(abs, { withFileTypes: true });
312
- const wanted = dirents.filter((e) => {
313
- let isDir = false;
314
- try { isDir = e.isDirectory(); } catch { isDir = false; }
315
- return isDir && !e.name.startsWith("$") && e.name !== "System Volume Information";
316
- });
317
- // mtime'lari PARALEL topla; donuk bir alt klasor 400ms sonra tarihsiz gecer.
318
- const entries = await Promise.all(
319
- wanted.map(async (e) => {
320
- const entryPath = path.join(abs, e.name);
321
- const st = await statSafe(entryPath, 400);
322
- let modifiedAt = null;
323
- if (st) { try { modifiedAt = st.mtime.toISOString(); } catch {} }
324
- return { name: e.name, path: entryPath, type: "folder", modifiedAt };
325
- })
326
- );
327
- entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
328
- const dirs = entries.map((entry) => entry.name);
329
- const isDriveRoot = /^[A-Za-z]:\\?$/.test(abs);
330
- const up = path.dirname(abs);
331
- const parent = isDriveRoot ? "" : up === abs ? null : up;
332
- const drives = process.platform === "win32" ? await listDrives() : [];
333
- return { path: abs, parent, dirs, drives, entries, places: await explorerPlaces(), warning };
334
- } catch (e) {
335
- const requested = p ? String(p) : "";
336
- return rootBrowse(requested ? `"${requested}" açılamadı; bu bilgisayar gösteriliyor.` : e.message);
337
- }
338
- }
339
-
340
- // ---- HTTP ----
341
- const server = http.createServer(async (req, res) => {
342
- const { pathname } = url.parse(req.url, true);
343
-
344
- // SSE
345
- if (pathname === "/api/events") {
346
- res.writeHead(200, {
347
- "Content-Type": "text/event-stream",
348
- "Cache-Control": "no-cache",
349
- Connection: "keep-alive",
350
- });
351
- res.write(`event: status\ndata: ${JSON.stringify(engine.status())}\n\n`);
352
- res.write(`event: queue\ndata: ${JSON.stringify(snapshot())}\n\n`);
353
- clients.add(res);
354
- req.on("close", () => clients.delete(res));
355
- return;
356
- }
357
-
358
- // API
359
- if (pathname.startsWith("/api/")) {
360
- try {
361
- if (pathname === "/api/state" && req.method === "GET") {
362
- const cfg = store.loadConfig();
363
- return send(res, 200, {
364
- status: engine.status(),
365
- queue: snapshot(),
366
- config: cfg,
367
- roles: store.listRoles(),
368
- skills: skillRegistry.allSkills().map((s) => ({ name: s.name, file: s.file, description: s.description, category: s.category, appliesTo: s.appliesTo, match: s.match })),
369
- cliStatus,
370
- schedules: cfg.schedules || [],
371
- platform: process.platform,
372
- workingDirAbs: path.resolve(store.WORK_BASE, cfg.workingDir || "."),
373
- });
374
- }
375
- if (pathname === "/api/security/autonomous-consent" && req.method === "POST") {
376
- const { accepted } = await readBody(req);
377
- if (accepted !== true) return send(res, 400, { error: "Acik kabul gerekli." });
378
- const cfg = store.loadConfig();
379
- cfg.autonomousConsentAcceptedAt = cfg.autonomousConsentAcceptedAt || new Date().toISOString();
380
- store.saveConfig(cfg);
381
- return send(res, 200, { accepted: true, acceptedAt: cfg.autonomousConsentAcceptedAt });
382
- }
383
- if (pathname === "/api/fs" && req.method === "GET") {
384
- return send(res, 200, await browseDir(url.parse(req.url, true).query.path));
385
- }
386
- if (pathname === "/api/tasks" && req.method === "POST") {
387
- const { prompt, targetDir, operatorCli, executionMode } = await readBody(req);
388
- if (!prompt || !prompt.trim()) return send(res, 400, { error: "prompt gerekli" });
389
- const cfg = store.loadConfig();
390
- // Operatör, uzman agent'lardan bağımsız bir CLI'dır (claude/codex/gemini/opencode);
391
- // operator.md rolüyle ekibi yönetir. Belirtilmemiş/geçersizse kurulu bir CLI'ye geçilir.
392
- const cliEntry = (name) => cliStatus.find((c) => c.id === name);
393
- const installedCli = (name) => name && cliRegistry.DEFINITIONS[name] && cliStatus.some((c) => c.id === name && c.installed
394
- && (name !== "opencode" || c.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)));
395
- const usableCli = (name) => {
396
- const entry = cliEntry(name);
397
- return installedCli(name) && (!entry?.health || entry.health.status === "ready");
398
- };
399
- let selectedCli = operatorCli || cfg.operator?.cli;
400
- if (!installedCli(selectedCli)) {
401
- if (cliRegistry.ensureValidOperator(cfg, cliStatus)) store.saveConfig(cfg);
402
- selectedCli = cfg.operator?.cli;
403
- }
404
- if (!installedCli(selectedCli)) {
405
- 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." });
406
- }
407
- if (!usableCli(selectedCli)) {
408
- const health = cliEntry(selectedCli)?.health;
409
- 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."}` });
410
- }
411
- const mode = ["auto", "fast", "balanced", "deep"].includes(executionMode) ? executionMode : "auto";
412
- const t = store.addTask(prompt.trim(), targetDir, selectedCli, mode);
413
- engine.wake();
414
- broadcast("queue", snapshot());
415
- return send(res, 200, t);
416
- }
417
- let m;
418
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/chat$/)) && req.method === "POST") {
419
- const found = store.findTask(m[1]);
420
- if (!found || found.state !== "done" || found.task.kind === "operator-chat") return send(res, 404, { error: "tamamlanmis gorev bulunamadi" });
421
- const { question } = await readBody(req);
422
- if (!question || !String(question).trim()) return send(res, 400, { error: "soru gerekli" });
423
- const chatTask = store.addChatTask(found.task, String(question).trim());
424
- if (!engine.status().running) engine.start();
425
- else engine.wake();
426
- broadcast("queue", snapshot());
427
- return send(res, 200, chatTask);
428
- }
429
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/(approve|reject)$/)) && req.method === "POST") {
430
- const found = store.findTask(m[1]);
431
- if (!found) return send(res, 404, { error: "gorev yok" });
432
- if (m[2] === "approve") {
433
- try { store.approveTask(m[1]); }
434
- catch (error) { return send(res, 409, { error: error.message }); }
435
- } else {
436
- try { store.rejectTask(m[1]); }
437
- catch (error) { return send(res, 409, { error: error.message }); }
438
- }
439
- broadcast("queue", snapshot());
440
- return send(res, 200, { ok: true });
441
- }
442
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/restore$/)) && req.method === "POST") {
443
- const found = store.findTask(m[1]);
444
- if (!found) return send(res, 404, { error: "gorev yok" });
445
- if (engine.status().current) return send(res, 409, { error: "Motor bir gorev calistirirken surum geri yuklenemez; once durdurun." });
446
- if (!found.task.checkpointId) return send(res, 400, { error: "Bu gorev icin kayitli bir surum yok." });
447
- const cfg = store.loadConfig();
448
- const result = checkpoints.restoreCheckpoint(found.task.checkpointId, { retention: cfg.versioningRetention });
449
- if (!result.ok) return send(res, 409, { error: result.error || "Surum geri yuklenemedi." });
450
- 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"}.` });
451
- broadcast("queue", snapshot());
452
- return send(res, 200, result);
453
- }
454
- if (pathname === "/api/checkpoints" && req.method === "GET") {
455
- const dir = url.parse(req.url, true).query.dir;
456
- return send(res, 200, checkpoints.listCheckpoints(dir ? path.resolve(store.WORK_BASE, dir) : undefined));
457
- }
458
- if (pathname === "/api/cli/discover" && req.method === "POST") {
459
- cliStatus = cliRegistry.discoverInstalled();
460
- const cfg = store.loadConfig();
461
- const body = await readBody(req);
462
- if (Array.isArray(body.ignoredAdapters)) {
463
- cfg.discoveryIgnoredAdapters = [...new Set(body.ignoredAdapters.filter((id) => cliRegistry.KNOWN_CLIS.includes(id)))];
464
- }
465
- let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
466
- if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
467
- if (changed || Array.isArray(body.ignoredAdapters)) store.saveConfig(cfg);
468
- await refreshCliHealth(true);
469
- return send(res, 200, { cliStatus, changed, config: store.loadConfig() });
470
- }
471
- if (pathname === "/api/cli/health" && req.method === "POST") {
472
- await refreshCliHealth(true);
473
- return send(res, 200, { cliStatus, config: store.loadConfig() });
474
- }
475
- if (pathname === "/api/codex/models" && req.method === "GET") {
476
- try { return send(res, 200, { models: await getCodexModels(false) }); }
477
- catch (error) { return send(res, 502, { error: `Codex modelleri alınamadı: ${error.message}` }); }
478
- }
479
- if (pathname === "/api/schedules" && req.method === "GET") {
480
- return send(res, 200, { schedules: store.loadConfig().schedules || [] });
481
- }
482
- if (pathname === "/api/schedules" && req.method === "POST") {
483
- const body = await readBody(req);
484
- let normalized;
485
- try { normalized = schedule.normalizeSchedule(body); }
486
- catch (error) { return send(res, 400, { error: error.message }); }
487
- if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
488
- normalized.id = newScheduleId();
489
- normalized.createdAt = new Date().toISOString();
490
- const next = schedule.computeNextRun(normalized);
491
- if (next) normalized.nextRunAt = next.toISOString();
492
- const cfg = store.loadConfig();
493
- cfg.schedules = [...(cfg.schedules || []), normalized];
494
- store.saveConfig(cfg);
495
- broadcastSchedules();
496
- return send(res, 200, normalized);
497
- }
498
- if ((m = pathname.match(/^\/api\/schedules\/([^/]+)$/)) && (req.method === "PUT" || req.method === "DELETE")) {
499
- const cfg = store.loadConfig();
500
- const list = cfg.schedules || [];
501
- const index = list.findIndex((s) => s.id === m[1]);
502
- if (index < 0) return send(res, 404, { error: "zamanlama yok" });
503
- if (req.method === "DELETE") {
504
- cfg.schedules = list.filter((s) => s.id !== m[1]);
505
- store.saveConfig(cfg);
506
- broadcastSchedules();
507
- return send(res, 200, { ok: true });
508
- }
509
- const body = await readBody(req);
510
- const current = list[index];
511
- // Yalnizca enabled toggle'i mi, yoksa tam guncelleme mi? Alanlari mevcutla harmanla.
512
- const merged = { ...current, ...body, id: current.id, createdAt: current.createdAt };
513
- let normalized;
514
- try { normalized = schedule.normalizeSchedule(merged); }
515
- catch (error) { return send(res, 400, { error: error.message }); }
516
- if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
517
- normalized.id = current.id;
518
- normalized.createdAt = current.createdAt;
519
- if (current.lastRunAt) normalized.lastRunAt = current.lastRunAt;
520
- if (current.lastTaskId) normalized.lastTaskId = current.lastTaskId;
521
- const next = normalized.enabled === false ? null : schedule.computeNextRun(normalized);
522
- if (next) normalized.nextRunAt = next.toISOString();
523
- else delete normalized.nextRunAt;
524
- cfg.schedules = list.map((s, i) => (i === index ? normalized : s));
525
- store.saveConfig(cfg);
526
- broadcastSchedules();
527
- return send(res, 200, normalized);
528
- }
529
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/events$/)) && req.method === "GET") {
530
- const limit = Number(url.parse(req.url, true).query.limit) || 1000;
531
- return send(res, 200, store.listRunEvents(m[1], Math.min(limit, 5000)));
532
- }
533
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "DELETE") {
534
- const found = store.findTask(m[1]);
535
- if (!found) return send(res, 404, { error: "gorev yok" });
536
- if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev silinemez" });
537
- store.removeTask(found.state, found.task.id);
538
- broadcast("queue", snapshot());
539
- return send(res, 200, { ok: true });
540
- }
541
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "PUT") {
542
- const found = store.findTask(m[1]);
543
- if (!found) return send(res, 404, { error: "gorev yok" });
544
- if (found.state !== "pending") return send(res, 409, { error: "yalnizca bekleyen gorev guncellenebilir" });
545
- if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev guncellenemez" });
546
- const body = await readBody(req);
547
- const cfg = store.loadConfig();
548
- const t = found.task;
549
- if (typeof body.prompt === "string") {
550
- if (!body.prompt.trim()) return send(res, 400, { error: "prompt bos olamaz" });
551
- t.prompt = body.prompt.trim();
552
- }
553
- if (body.executionMode && ["auto", "fast", "balanced", "deep"].includes(body.executionMode)) t.executionMode = body.executionMode;
554
- if (body.operatorCli) {
555
- 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" });
556
- t.operatorCli = body.operatorCli;
557
- }
558
- if (typeof body.targetDir === "string") {
559
- if (body.targetDir.trim()) t.targetDir = body.targetDir.trim();
560
- else delete t.targetDir;
561
- }
562
- // Plan yeniden uretilsin: kullanici hedefi degistirdiyse eski plan gecersizdir.
563
- delete t.teamState; delete t.planPreview; delete t.planHash; delete t.approved;
564
- store.saveTask("pending", t);
565
- broadcast("queue", snapshot());
566
- return send(res, 200, t);
567
- }
568
- if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "GET") {
569
- const found = store.findTask(m[1]);
570
- return found ? send(res, 200, found) : send(res, 404, { error: "gorev yok" });
571
- }
572
- if (pathname === "/api/engine" && req.method === "POST") {
573
- const { action, mode } = await readBody(req);
574
- if (mode) engine.setMode(mode);
575
- if (action === "start") {
576
- if (!store.loadConfig().autonomousConsentAcceptedAt) return send(res, 409, { error: "Otonom CLI kosullarini once onaylayin.", code: "AUTONOMOUS_CONSENT_REQUIRED" });
577
- engine.start();
578
- }
579
- if (action === "stop") engine.stop();
580
- return send(res, 200, engine.status());
581
- }
582
- if (pathname === "/api/config" && req.method === "GET") {
583
- return send(res, 200, store.loadConfig());
584
- }
585
- if (pathname === "/api/config" && req.method === "PUT") {
586
- let cfg = await readBody(req);
587
- if (!cfg.agents || typeof cfg.agents !== "object") return send(res, 400, { error: "agents nesnesi gerekli" });
588
- if (Object.keys(cfg.agents).length < 1) return send(res, 400, { error: "operatorun altinda calisacak en az bir uzman agent gerekli" });
589
- 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)" });
590
- cfg.operator.roleFile = "roles/operator.md";
591
- delete cfg.operator.agent;
592
- // Eski UI govdeleri (operator.codexSettings / operator.model) yeni cliSettings
593
- // yapisina tasinir; dogrulama tek semada yapilir.
594
- cfg = store.normalizeConfig(cfg);
595
- cliRegistry.normalizeAgentAdapters(cfg);
596
- 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" });
597
- 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" });
598
- for (const id of ["codex", "opencode"]) {
599
- if (cfg.cliSettings[id].model != null && typeof cfg.cliSettings[id].model !== "string") return send(res, 400, { error: `cliSettings.${id}.model metin olmali` });
600
- }
601
- if (cfg.skills !== undefined) {
602
- if (typeof cfg.skills !== "object" || cfg.skills === null) return send(res, 400, { error: "skills nesne olmali" });
603
- 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" });
604
- 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" });
605
- 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" });
606
- for (const key of ["catalogLimit", "maxSkillsPerAssignment"]) {
607
- 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` });
608
- }
609
- if (cfg.skills.autoMatch !== undefined && typeof cfg.skills.autoMatch !== "boolean") return send(res, 400, { error: "skills.autoMatch boolean olmali" });
610
- }
611
- for (const [name, agent] of Object.entries(cfg.agents)) {
612
- if (!name.trim() || !agent.cmd || typeof agent.cmd !== "string") return send(res, 400, { error: `gecersiz agent: ${name}` });
613
- if (!Array.isArray(agent.args)) return send(res, 400, { error: `${name}.args dizi olmali` });
614
- }
615
- if (cfg.notify !== undefined) {
616
- if (typeof cfg.notify !== "object" || cfg.notify === null) return send(res, 400, { error: "notify nesne olmali" });
617
- const url = String(cfg.notify.webhookUrl || "").trim();
618
- if (url && !/^https?:\/\//i.test(url)) return send(res, 400, { error: "notify.webhookUrl http(s) ile baslamali veya bos olmali" });
619
- }
620
- store.saveConfig(cfg);
621
- broadcast("status", engine.status());
622
- return send(res, 200, { ok: true });
623
- }
624
- if (pathname === "/api/skills" && req.method === "GET") {
625
- 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 })) });
626
- }
627
- if ((m = pathname.match(/^\/api\/skills\/(.+)$/))) {
628
- let file = decodeURIComponent(m[1]);
629
- if (!file.endsWith(".md")) file += ".md";
630
- if (path.basename(file) !== file) return send(res, 400, { error: "gecersiz beceri adi" });
631
- if (req.method === "GET") return send(res, 200, { file, content: skillRegistry.readRaw(file) });
632
- if (req.method === "PUT") {
633
- const { content } = await readBody(req);
634
- const validation = skillRegistry.validateSkill(file, content || "");
635
- if (!validation.ok) return send(res, 400, { error: validation.errors.join("; ") });
636
- const saved = skillRegistry.writeSkill(file, content);
637
- return send(res, 200, { ok: true, file: saved });
638
- }
639
- if (req.method === "DELETE") {
640
- // Silmeden once beceri adini coz; etkin liste dosya adini degil frontmatter `name`i tutar.
641
- const existing = skillRegistry.loadSkill(file);
642
- const names = new Set([file.replace(/\.md$/i, ""), existing?.name].filter(Boolean));
643
- skillRegistry.deleteSkill(file);
644
- const cfg = store.loadConfig();
645
- if (Array.isArray(cfg.skills?.enabled) && cfg.skills.enabled.some((name) => names.has(name))) {
646
- cfg.skills.enabled = cfg.skills.enabled.filter((name) => !names.has(name));
647
- store.saveConfig(cfg);
648
- }
649
- return send(res, 200, { ok: true });
650
- }
651
- }
652
- if ((m = pathname.match(/^\/api\/roles\/(.+)$/))) {
653
- let file = decodeURIComponent(m[1]);
654
- if (!file.endsWith(".md")) file += ".md";
655
- if (req.method === "GET") return send(res, 200, { file, content: store.readRole(file) });
656
- if (req.method === "PUT") {
657
- const { content } = await readBody(req);
658
- store.writeRole(file, content || "");
659
- return send(res, 200, { ok: true, file });
660
- }
661
- if (req.method === "DELETE") {
662
- store.deleteRole(file);
663
- return send(res, 200, { ok: true });
664
- }
665
- }
666
- return send(res, 404, { error: "bilinmeyen endpoint" });
667
- } catch (e) {
668
- return send(res, 500, { error: e.message });
669
- }
670
- }
671
-
672
- // Statik
673
- serveStatic(res, pathname === "/" ? "index.html" : pathname.slice(1));
674
- });
675
-
676
- function openBrowser(target) {
677
- if (process.env.OPEN === "0" || process.env.NO_OPEN) return;
678
- const { spawn } = require("child_process");
679
- const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
680
- const args = process.platform === "win32" ? ["/c", "start", "", target] : [target];
681
- try { spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }).unref(); } catch {}
682
- }
683
-
684
- function startupBanner() {
685
- const G = "\x1b[32m", Y = "\x1b[33m", DIM = "\x1b[90m", B = "\x1b[1m", C = "\x1b[36m", X = "\x1b[0m";
686
- const installed = cliStatus.filter((c) => c.installed);
687
- const cfg = store.loadConfig();
688
- const url = `http://${HOST}:${PORT}`;
689
- console.log(`\n ${B}${C}CrewCtl${X}`);
690
- console.log(` ${DIM}operatör-liderliğinde çok-agent orkestratörü${X}\n`);
691
- const cliLine = cliStatus.map((c) => `${c.installed ? G + "" : DIM + "○"} ${c.id}${X}`).join(" ");
692
- console.log(` CLI: ${cliLine}`);
693
- console.log(` Operatör: ${cfg.operator?.cli || DIM + "(seçilemedi)" + X} ${DIM}·${X} Uzman: ${Object.values(cfg.agents).filter((a) => a.enabled !== false).length}`);
694
- if (!installed.length) {
695
- console.log(`\n ${Y}⚠ Hiçbir CLI kurulu değil.${X} Codex / Claude / Gemini / OpenCode'dan en az birini kurun.`);
696
- console.log(` ${DIM}Ayrıntı için: npm run doctor${X}`);
697
- }
698
- console.log(`\n ${B}▶ Panel: ${C}${url}${X}`);
699
- 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}`);
700
- // npx gecici onbellekten calisiyorsa kalici 'crewctl' komutu kurulmamistir; kullaniciya nasil
701
- // kalici hale getirecegini hatirlat (aksi halde kapatinca 'crewctl' bulunamaz).
702
- if (/[\\/]_npx[\\/]/.test(__dirname)) {
703
- console.log(` ${DIM}Kalıcı 'crewctl' komutu için: ${Y}npm i -g @omerrgocmen/crewctl${X}`);
704
- }
705
- console.log("");
706
- openBrowser(url);
707
- }
708
-
709
- // ---- Zamanlanmis gorev tik'i ----
710
- // Zamani gelen zamanlamalari kuyruga alir. Salt hesap schedule.js'te; burada yalnizca yan
711
- // etkiler var. Motor duruyorsa gorev yalnizca 'pending'e girer (manuel ekleme davranisi);
712
- // otomatik baslatma yoktur. engine.wake() yalnizca calisan dongyu erkenden uyandirir.
713
- const SCHEDULER_TICK_MS = 30000;
714
- let schedulerRunning = false;
715
- function schedulerTick() {
716
- if (schedulerRunning) return;
717
- schedulerRunning = true;
718
- try {
719
- const cfg = store.loadConfig();
720
- const now = new Date();
721
- const due = schedule.dueSchedules(cfg.schedules || [], now);
722
- if (!due.length) return;
723
- let firedAny = false;
724
- for (const item of cfg.schedules || []) {
725
- if (!due.some((d) => d.id === item.id)) continue;
726
- try {
727
- const task = store.addScheduledTask(item);
728
- item.lastRunAt = now.toISOString();
729
- item.lastTaskId = task.id;
730
- const next = item.enabled === false ? null : schedule.computeNextRun(item, now);
731
- if (next) item.nextRunAt = next.toISOString();
732
- else delete item.nextRunAt;
733
- firedAny = true;
734
- 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)}` });
735
- } catch (error) {
736
- console.error("Zamanlanmis gorev olusturulamadi:", error.message);
737
- }
738
- }
739
- if (firedAny) {
740
- store.saveConfig(cfg);
741
- broadcast("queue", snapshot());
742
- broadcastSchedules();
743
- engine.wake();
744
- }
745
- } catch (error) {
746
- console.error("Zamanlayici tik hatasi:", error.message);
747
- } finally {
748
- schedulerRunning = false;
749
- }
750
- }
751
- const schedulerTimer = setInterval(schedulerTick, SCHEDULER_TICK_MS);
752
- if (schedulerTimer && typeof schedulerTimer.unref === "function") schedulerTimer.unref();
753
-
754
- server.listen(PORT, HOST, startupBanner);
755
- setImmediate(() => refreshCliHealth(false).catch((err) => console.error("CLI sağlık testi başlatılamadı:", err.message)));
756
- setTimeout(schedulerTick, 5000).unref?.();
757
- setImmediate(() => {
758
- if (codexModelRefreshDue) getCodexModels(true).catch((err) => console.error("Codex model kataloğu yenilenemedi:", err.message));
759
- });
760
- server.on("error", (err) => {
761
- if (err.code === "EADDRINUSE") {
762
- console.error(`\n \x1b[31m✗ ${HOST}:${PORT} kullanımda.\x1b[0m Farklı port ile deneyin: \x1b[1mPORT=4318 npm start\x1b[0m\n`);
763
- process.exit(1);
764
- }
765
- throw err;
766
- });
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
+ let cliStatus = cliRegistry.discoverInstalled({
49
+ cache: store.loadConfig().cliDiscoveryCache,
50
+ openCodeModels: store.loadConfig().openCodeModelCache?.models,
51
+ });
52
+ {
53
+ const cfg = store.loadConfig();
54
+ const nextDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
55
+ const discoveryCacheChanged = JSON.stringify((cfg.cliDiscoveryCache || {}).results || {}) !== JSON.stringify(nextDiscoveryCache.results);
56
+ if (discoveryCacheChanged) cfg.cliDiscoveryCache = nextDiscoveryCache;
57
+ const savedModels = cfg.codexModelCache && typeof cfg.codexModelCache === "object" ? cfg.codexModelCache : {};
58
+ const startupCount = Number(savedModels.startupCount || 0) + 1;
59
+ codexModelCache = {
60
+ checkedAt: Date.parse(savedModels.checkedAt || "") || 0,
61
+ models: Array.isArray(savedModels.models) ? savedModels.models : [],
62
+ };
63
+ codexModelRefreshDue = !codexModelCache.models.length || startupCount % 10 === 0;
64
+ cfg.codexModelCache = { ...savedModels, startupCount, lastStartedAt: new Date().toISOString() };
65
+ const healthCacheValid = cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION;
66
+ const cached = healthCacheValid ? cfg.cliHealthCache?.results : null;
67
+ let staleHealthCleared = false;
68
+ if (cached && typeof cached === "object") {
69
+ cliStatus = cliStatus.map((cli) => cached[cli.id] ? { ...cli, health: cached[cli.id] } : cli);
70
+ } else {
71
+ for (const agent of Object.values(cfg.agents || {})) {
72
+ if (agent.health) { delete agent.health; staleHealthCleared = true; }
73
+ }
74
+ }
75
+ let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
76
+ if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
77
+ const openCodeModelsChanged = persistOpenCodeModels(cfg, cliStatus);
78
+ if (changed || staleHealthCleared || discoveryCacheChanged || openCodeModelsChanged || JSON.stringify(savedModels) !== JSON.stringify(cfg.codexModelCache)) store.saveConfig(cfg);
79
+ }
80
+
81
+ // ---- SSE istemcileri ----
82
+ const clients = new Set();
83
+ function broadcast(event, data) {
84
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
85
+ for (const res of clients) {
86
+ try {
87
+ res.write(payload);
88
+ } catch {}
89
+ }
90
+ }
91
+ engine.on("log", (d) => broadcast("log", d));
92
+ engine.on("status", (d) => broadcast("status", d));
93
+ engine.on("result", (d) => broadcast("result", d));
94
+ engine.on("activity", (d) => broadcast("activity", d));
95
+ engine.on("message", (d) => broadcast("message", d));
96
+ engine.on("filechange", (d) => broadcast("filechange", d));
97
+ engine.on("queue", () => broadcast("queue", snapshot()));
98
+ engine.on("notify", (d) => { sendNotification(d); broadcast("notify", d); });
99
+
100
+ // Görev bitince/başarısız olunca kullanıcının yapılandırdığı webhook'a POST atar (Slack incoming
101
+ // webhook formatıyla uyumlu: {text}). Tamamen opsiyonel ve ateşle-unut; Node'un yerleşik fetch'ini
102
+ // kullanır (yeni bağımlılık yok). Hata görevi asla etkilemez. Yapılandırılmamışsa sessizce atlanır.
103
+ function sendNotification(outcome) {
104
+ try {
105
+ const cfg = store.loadConfig();
106
+ const n = cfg.notify || {};
107
+ const url = String(n.webhookUrl || "").trim();
108
+ if (!url || !/^https?:\/\//i.test(url)) return;
109
+ if (outcome.status === "failed" ? n.onFailed === false : n.onComplete === false) return;
110
+ const icon = outcome.status === "failed" ? "❌" : "✅";
111
+ const head = outcome.status === "failed" ? "Görev başarısız" : "Görev tamamlandı";
112
+ const detail = outcome.status === "failed"
113
+ ? (outcome.error ? `\nHata: ${String(outcome.error).slice(0, 300)}` : "")
114
+ : `${outcome.operator ? ` · ${outcome.operator}` : ""}${outcome.rounds ? ` · ${outcome.rounds} tur` : ""}${Number.isFinite(outcome.fileCount) ? ` · ${outcome.fileCount} dosya` : ""}${outcome.verification ? ` · ✓ ${String(outcome.verification).slice(0, 120)}` : ""}`;
115
+ const text = `${icon} CrewCtl — ${head}: ${String(outcome.prompt || outcome.id).slice(0, 200)}${detail}`;
116
+ const controller = new AbortController();
117
+ const timer = setTimeout(() => controller.abort(), 8000);
118
+ fetch(url, {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({ text, crewctl: outcome }),
122
+ signal: controller.signal,
123
+ }).catch(() => {}).finally(() => clearTimeout(timer));
124
+ } catch {}
125
+ }
126
+
127
+ function broadcastSchedules() {
128
+ broadcast("schedules", store.loadConfig().schedules || []);
129
+ }
130
+
131
+ // Pano/kuyruk listesi yalnizca ozet alanlari gosterir; teamState (tum agent ciktilari/mesajlari),
132
+ // conversation ve changes gibi agir ic alanlar liste yukunu gereksiz sisirir ve her SSE queue
133
+ // yayininda serilestirilir. Detay gorunumleri tam gorevi ayri /api/tasks/:id ile ceker; burada
134
+ // bu agir alanlari cikartip yayin boyutunu kuculturuz. (planPreview/delivery/summary korunur.)
135
+ function liteTask(task) {
136
+ const { teamState, conversation, changes, ...rest } = task;
137
+ return rest;
138
+ }
139
+ function snapshot() {
140
+ return {
141
+ pending: store.listTasks("pending").map(liteTask),
142
+ approval: store.listTasks("approval").map(liteTask),
143
+ // Operator-chat gorevleri de done'a yazildigi icin filtreden sonra 30 gercek gorev kalsin
144
+ // diye biraz genis bir pencere (80) okunur; yine de sabit ve gecmisten bagimsizdir.
145
+ done: store.listRecentTasks("done", 80).filter((task) => task.kind !== "operator-chat").slice(-30).reverse().map(liteTask),
146
+ failed: store.listRecentTasks("failed", 30).reverse().map(liteTask),
147
+ };
148
+ }
149
+
150
+ function newScheduleId() {
151
+ return "sch-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7);
152
+ }
153
+ // Zamanlamanin operatorCli'si (verilmisse) kurulu bilinen bir CLI olmali; verilmezse gorev
154
+ // calisma aninda config operatorune duser (gorev olusturmayla ayni davranis).
155
+ function scheduleOperatorValid(name) {
156
+ if (!name) return true;
157
+ return Boolean(cliRegistry.DEFINITIONS[name]) && cliStatus.some((c) => c.id === name && c.installed);
158
+ }
159
+
160
+ function applyHealthToConfig(results) {
161
+ const cfg = store.loadConfig();
162
+ let changed = false;
163
+ const cachedResults = {};
164
+ for (const result of results) {
165
+ cachedResults[result.id] = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
166
+ for (const agent of Object.values(cfg.agents || {})) {
167
+ if ((agent.adapter || cliRegistry.adapterId(agent.cmd)) !== result.id) continue;
168
+ const next = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
169
+ if (JSON.stringify(agent.health || {}) !== JSON.stringify(next)) {
170
+ agent.health = next;
171
+ changed = true;
172
+ }
173
+ }
174
+ }
175
+ const nextCache = { version: HEALTH_CACHE_VERSION, checkedAt: new Date().toISOString(), results: cachedResults };
176
+ if (JSON.stringify(cfg.cliHealthCache || {}) !== JSON.stringify(nextCache)) {
177
+ cfg.cliHealthCache = nextCache;
178
+ changed = true;
179
+ }
180
+ if (changed) store.saveConfig(cfg);
181
+ return cfg;
182
+ }
183
+
184
+ let healthRunning = false;
185
+ // Motor bir gorev calistirirken (or. operatör codex) ayni CLI'ye karsi canli probe acmak
186
+ // codex'in ikinci exec/app-server oturumunu SIGTERM ile kesmesine yol acar. Probe'lar en iyi
187
+ // cabadir; motor mesgulken canli probe atlanip son bilinen sonuc dondurulur.
188
+ function engineBusy() {
189
+ try { return Boolean(engine.status().current); } catch { return false; }
190
+ }
191
+ async function getCodexModels(force = false) {
192
+ if (!force && codexModelCache.models.length && Date.now() - codexModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return codexModelCache.models;
193
+ if (engineBusy()) return codexModelCache.models;
194
+ const models = await cliRegistry.listCodexModels({ timeoutMs: 20000 });
195
+ codexModelCache = { checkedAt: Date.now(), models };
196
+ const cfg = store.loadConfig();
197
+ cfg.codexModelCache = { ...(cfg.codexModelCache || {}), checkedAt: new Date().toISOString(), models };
198
+ store.saveConfig(cfg);
199
+ return models;
200
+ }
201
+ async function refreshCliHealth(force = false) {
202
+ if (healthRunning) return cliStatus;
203
+ const cfg = store.loadConfig();
204
+ const cachedAt = Date.parse(cfg.cliHealthCache?.checkedAt || "");
205
+ if (!force && cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION && Number.isFinite(cachedAt) && Date.now() - cachedAt < HEALTH_CACHE_TTL_MS) {
206
+ broadcast("cli-health", cliStatus);
207
+ return cliStatus;
208
+ }
209
+ if (engineBusy()) {
210
+ broadcast("cli-health", cliStatus);
211
+ return cliStatus;
212
+ }
213
+ healthRunning = true;
214
+ cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
215
+ broadcast("cli-health", cliStatus);
216
+ const generation = cliRegistry.currentProbeGeneration();
217
+ try {
218
+ const results = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
219
+ // Motor bu sirada bir goreve basladiysa probe'lar SIGKILL edilmistir; elimizdeki
220
+ // "failed/timeout" sonuclari gercek degil, iptalin yan urunudur. Onbellege yazmak
221
+ // CLI'yi 6 saat boyunca yanlislikla arizali gosterirdi. Durumu bilinmiyor'a cekip
222
+ // (engellemeyen gecici durum) bir sonraki tetiklemede yeniden olcmeyi bekleriz.
223
+ if (cliRegistry.currentProbeGeneration() !== generation) {
224
+ cliStatus = cliStatus.map((cli) => ({
225
+ ...cli,
226
+ health: { status: "unknown", label: "Bilinmiyor", detail: "Sağlık testi, bir görev başladığı için yarıda kesildi; sonuç kaydedilmedi." },
227
+ }));
228
+ broadcast("cli-health", cliStatus);
229
+ return cliStatus;
230
+ }
231
+ cliStatus = results;
232
+ applyHealthToConfig(cliStatus);
233
+ broadcast("cli-health", cliStatus);
234
+ return cliStatus;
235
+ } finally {
236
+ healthRunning = false;
237
+ }
238
+ }
239
+
240
+ // ---- yardimcilar ----
241
+ function send(res, code, obj) {
242
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
243
+ res.end(JSON.stringify(obj));
244
+ }
245
+ function readBody(req) {
246
+ return new Promise((resolve) => {
247
+ let b = "";
248
+ req.on("data", (c) => (b += c));
249
+ req.on("end", () => {
250
+ try {
251
+ resolve(b ? JSON.parse(b) : {});
252
+ } catch {
253
+ resolve({});
254
+ }
255
+ });
256
+ });
257
+ }
258
+ 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" };
259
+ function serveStatic(res, file) {
260
+ const p = path.resolve(WEB, file);
261
+ if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
262
+ res.writeHead(404);
263
+ return res.end("not found");
264
+ }
265
+ res.writeHead(200, { "Content-Type": MIME[path.extname(p)] || "text/plain" });
266
+ fs.createReadStream(p).pipe(res);
267
+ }
268
+
269
+ // ---- Klasor gezgini (async + timeout) ----
270
+ // ONEMLI: eskiden statSync/readdirSync kullaniliyordu. Kopuk bir AG/eslesmis surucu
271
+ // (or. dead SMB share) statSync'te saniyelerce blokladigi icin TEK is parcacikli sunucu
272
+ // tamamen kitleniyordu (Gozat "acilmiyor"/donuyor). Artik tum FS erisimi async ve her
273
+ // cagri kisa bir timeout'a yaristirilir; donuk surucu bloklamaz, atlanir.
274
+ let driveCache = { at: 0, value: [] };
275
+ function statSafe(p, ms = 700) {
276
+ return Promise.race([
277
+ fs.promises.stat(p).catch(() => null),
278
+ new Promise((resolve) => setTimeout(() => resolve(null), ms)),
279
+ ]);
280
+ }
281
+ async function uniqExistingDirs(items) {
282
+ const seen = new Set();
283
+ const out = [];
284
+ for (const item of items) {
285
+ if (!item.path || seen.has(item.path.toLowerCase())) continue;
286
+ seen.add(item.path.toLowerCase());
287
+ const st = await statSafe(item.path);
288
+ if (st && st.isDirectory()) out.push(item);
289
+ }
290
+ return out;
291
+ }
292
+ async function listDrives() {
293
+ if (Date.now() - driveCache.at < 30000) return driveCache.value.slice();
294
+ const found = new Set();
295
+ for (const d of [process.env.SystemDrive, process.env.HOMEDRIVE]) {
296
+ if (d) found.add(path.parse(d).root || `${d.replace(/[\\/]$/, "")}\\`);
297
+ }
298
+ // Tum surucu harflerini PARALEL yokla; donuk surucu 700ms sonra atlanir (bloklamaz).
299
+ const results = await Promise.all(
300
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map(async (c) => {
301
+ const d = c + ":\\";
302
+ const st = await statSafe(d, 700);
303
+ return st && st.isDirectory() ? d : null;
304
+ })
305
+ );
306
+ for (const d of results) if (d) found.add(d);
307
+ const drives = [...found].sort((a, b) => a.localeCompare(b));
308
+ driveCache = { at: Date.now(), value: drives };
309
+ return drives.slice();
310
+ }
311
+ async function explorerPlaces() {
312
+ const home = os.homedir();
313
+ const oneDrive = process.env.OneDrive || process.env.OneDriveConsumer || process.env.OneDriveCommercial;
314
+ const candidates = [
315
+ { id: "home", label: "Ana klasör", icon: "⌂", path: home },
316
+ { id: "desktop", label: "Masaüstü", icon: "▣", path: path.join(home, "Desktop") },
317
+ oneDrive && { id: "onedrive-desktop", label: "OneDrive Masaüstü", icon: "▣", path: path.join(oneDrive, "Desktop") },
318
+ { id: "documents", label: "Belgeler", icon: "▤", path: path.join(home, "Documents") },
319
+ oneDrive && { id: "onedrive-documents", label: "OneDrive Belgeler", icon: "▤", path: path.join(oneDrive, "Documents") },
320
+ { id: "downloads", label: "İndirilenler", icon: "↓", path: path.join(home, "Downloads") },
321
+ { id: "project", label: "Çalışma klasörü", icon: "◇", path: store.WORK_BASE },
322
+ ].filter(Boolean);
323
+ return uniqExistingDirs(candidates);
324
+ }
325
+ async function rootBrowse(message) {
326
+ if (process.platform === "win32") {
327
+ const drives = await listDrives();
328
+ return {
329
+ path: "",
330
+ parent: null,
331
+ dirs: drives,
332
+ drives,
333
+ entries: drives.map((drive) => ({ name: drive, path: drive, type: "drive", modifiedAt: null })),
334
+ places: await explorerPlaces(),
335
+ isRoot: true,
336
+ warning: message || "",
337
+ };
338
+ }
339
+ return browseDir("/", message);
340
+ }
341
+ async function browseDir(p, warning = "") {
342
+ try {
343
+ if (!p) return await rootBrowse(warning);
344
+ const abs = path.resolve(p);
345
+ const rootStat = await statSafe(abs, 2000);
346
+ if (!rootStat || !rootStat.isDirectory()) throw new Error("Klasör değil");
347
+ const dirents = await fs.promises.readdir(abs, { withFileTypes: true });
348
+ const wanted = dirents.filter((e) => {
349
+ let isDir = false;
350
+ try { isDir = e.isDirectory(); } catch { isDir = false; }
351
+ return isDir && !e.name.startsWith("$") && e.name !== "System Volume Information";
352
+ });
353
+ // mtime'lari PARALEL topla; donuk bir alt klasor 400ms sonra tarihsiz gecer.
354
+ const entries = await Promise.all(
355
+ wanted.map(async (e) => {
356
+ const entryPath = path.join(abs, e.name);
357
+ const st = await statSafe(entryPath, 400);
358
+ let modifiedAt = null;
359
+ if (st) { try { modifiedAt = st.mtime.toISOString(); } catch {} }
360
+ return { name: e.name, path: entryPath, type: "folder", modifiedAt };
361
+ })
362
+ );
363
+ entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
364
+ const dirs = entries.map((entry) => entry.name);
365
+ const isDriveRoot = /^[A-Za-z]:\\?$/.test(abs);
366
+ const up = path.dirname(abs);
367
+ const parent = isDriveRoot ? "" : up === abs ? null : up;
368
+ const drives = process.platform === "win32" ? await listDrives() : [];
369
+ return { path: abs, parent, dirs, drives, entries, places: await explorerPlaces(), warning };
370
+ } catch (e) {
371
+ const requested = p ? String(p) : "";
372
+ return rootBrowse(requested ? `"${requested}" açılamadı; bu bilgisayar gösteriliyor.` : e.message);
373
+ }
374
+ }
375
+
376
+ // ---- HTTP ----
377
+ const server = http.createServer(async (req, res) => {
378
+ const { pathname } = url.parse(req.url, true);
379
+
380
+ // SSE
381
+ if (pathname === "/api/events") {
382
+ res.writeHead(200, {
383
+ "Content-Type": "text/event-stream",
384
+ "Cache-Control": "no-cache",
385
+ Connection: "keep-alive",
386
+ });
387
+ res.write(`event: status\ndata: ${JSON.stringify(engine.status())}\n\n`);
388
+ res.write(`event: queue\ndata: ${JSON.stringify(snapshot())}\n\n`);
389
+ clients.add(res);
390
+ req.on("close", () => clients.delete(res));
391
+ return;
392
+ }
393
+
394
+ // API
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.
508
+ const prevCfg = store.loadConfig();
509
+ cliStatus = cliRegistry.discoverInstalled({ force: true, openCodeModels: prevCfg.openCodeModelCache?.models });
510
+ const cfg = store.loadConfig();
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
+ // Statik
725
+ serveStatic(res, pathname === "/" ? "index.html" : pathname.slice(1));
726
+ });
727
+
728
+ function openBrowser(target) {
729
+ if (process.env.OPEN === "0" || process.env.NO_OPEN) return;
730
+ const { spawn } = require("child_process");
731
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
732
+ const args = process.platform === "win32" ? ["/c", "start", "", target] : [target];
733
+ try { spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }).unref(); } catch {}
734
+ }
735
+
736
+ function startupBanner() {
737
+ const G = "\x1b[32m", Y = "\x1b[33m", DIM = "\x1b[90m", B = "\x1b[1m", C = "\x1b[36m", X = "\x1b[0m";
738
+ const installed = cliStatus.filter((c) => c.installed);
739
+ const cfg = store.loadConfig();
740
+ const url = `http://${HOST}:${PORT}`;
741
+ console.log(`\n ${B}${C}CrewCtl${X}`);
742
+ console.log(` ${DIM}operatör-liderliğinde çok-agent orkestratörü${X}\n`);
743
+ const cliLine = cliStatus.map((c) => `${c.installed ? G + "●" : DIM + "○"} ${c.id}${X}`).join(" ");
744
+ console.log(` CLI: ${cliLine}`);
745
+ console.log(` Operatör: ${cfg.operator?.cli || DIM + "(seçilemedi)" + X} ${DIM}·${X} Uzman: ${Object.values(cfg.agents).filter((a) => a.enabled !== false).length}`);
746
+ if (!installed.length) {
747
+ console.log(`\n ${Y} Hiçbir CLI kurulu değil.${X} Codex / Claude / Gemini / OpenCode'dan en az birini kurun.`);
748
+ console.log(` ${DIM}Ayrıntı için: npm run doctor${X}`);
749
+ }
750
+ console.log(`\n ${B}▶ Panel: ${C}${url}${X}`);
751
+ 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}`);
752
+ // npx gecici onbellekten calisiyorsa kalici 'crewctl' komutu kurulmamistir; kullaniciya nasil
753
+ // kalici hale getirecegini hatirlat (aksi halde kapatinca 'crewctl' bulunamaz).
754
+ if (/[\\/]_npx[\\/]/.test(__dirname)) {
755
+ console.log(` ${DIM}Kalıcı 'crewctl' komutu için: ${Y}npm i -g @omerrgocmen/crewctl${X}`);
756
+ }
757
+ console.log("");
758
+ openBrowser(url);
759
+ }
760
+
761
+ // ---- Zamanlanmis gorev tik'i ----
762
+ // Zamani gelen zamanlamalari kuyruga alir. Salt hesap schedule.js'te; burada yalnizca yan
763
+ // etkiler var. Motor duruyorsa gorev yalnizca 'pending'e girer (manuel ekleme davranisi);
764
+ // otomatik baslatma yoktur. engine.wake() yalnizca calisan dongyu erkenden uyandirir.
765
+ const SCHEDULER_TICK_MS = 30000;
766
+ let schedulerRunning = false;
767
+ function schedulerTick() {
768
+ if (schedulerRunning) return;
769
+ schedulerRunning = true;
770
+ try {
771
+ const cfg = store.loadConfig();
772
+ const now = new Date();
773
+ const due = schedule.dueSchedules(cfg.schedules || [], now);
774
+ if (!due.length) return;
775
+ let firedAny = false;
776
+ for (const item of cfg.schedules || []) {
777
+ if (!due.some((d) => d.id === item.id)) continue;
778
+ try {
779
+ const task = store.addScheduledTask(item);
780
+ item.lastRunAt = now.toISOString();
781
+ item.lastTaskId = task.id;
782
+ const next = item.enabled === false ? null : schedule.computeNextRun(item, now);
783
+ if (next) item.nextRunAt = next.toISOString();
784
+ else delete item.nextRunAt;
785
+ firedAny = true;
786
+ 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)}` });
787
+ } catch (error) {
788
+ console.error("Zamanlanmis gorev olusturulamadi:", error.message);
789
+ }
790
+ }
791
+ if (firedAny) {
792
+ store.saveConfig(cfg);
793
+ broadcast("queue", snapshot());
794
+ broadcastSchedules();
795
+ engine.wake();
796
+ }
797
+ } catch (error) {
798
+ console.error("Zamanlayici tik hatasi:", error.message);
799
+ } finally {
800
+ schedulerRunning = false;
801
+ }
802
+ }
803
+ const schedulerTimer = setInterval(schedulerTick, SCHEDULER_TICK_MS);
804
+ if (schedulerTimer && typeof schedulerTimer.unref === "function") schedulerTimer.unref();
805
+
806
+ server.listen(PORT, HOST, startupBanner);
807
+ setImmediate(() => refreshCliHealth(false).catch((err) => console.error("CLI sağlık testi başlatılamadı:", err.message)));
808
+ setTimeout(schedulerTick, 5000).unref?.();
809
+ setImmediate(() => {
810
+ if (codexModelRefreshDue) getCodexModels(true).catch((err) => console.error("Codex model kataloğu yenilenemedi:", err.message));
811
+ });
812
+ server.on("error", (err) => {
813
+ if (err.code === "EADDRINUSE") {
814
+ console.error(`\n \x1b[31m✗ ${HOST}:${PORT} kullanımda.\x1b[0m Farklı port ile deneyin: \x1b[1mPORT=4318 npm start\x1b[0m\n`);
815
+ process.exit(1);
816
+ }
817
+ throw err;
818
+ });