@omerrgocmen/crewctl 1.0.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 (95) hide show
  1. package/README.md +182 -0
  2. package/orchestrator/LICENSE +21 -0
  3. package/orchestrator/README.md +452 -0
  4. package/orchestrator/config.default.json +65 -0
  5. package/orchestrator/roles/executor.md +49 -0
  6. package/orchestrator/roles/operator-chat.md +25 -0
  7. package/orchestrator/roles/operator.md +75 -0
  8. package/orchestrator/roles/planner.md +53 -0
  9. package/orchestrator/roles/reviewer.md +54 -0
  10. package/orchestrator/skills/acceptance-criteria.md +18 -0
  11. package/orchestrator/skills/accessibility-audit.md +16 -0
  12. package/orchestrator/skills/accessible-forms.md +16 -0
  13. package/orchestrator/skills/api-design.md +27 -0
  14. package/orchestrator/skills/api-documentation.md +16 -0
  15. package/orchestrator/skills/architecture-decision-record.md +18 -0
  16. package/orchestrator/skills/authentication-design.md +16 -0
  17. package/orchestrator/skills/authorization-review.md +16 -0
  18. package/orchestrator/skills/backward-compatibility.md +17 -0
  19. package/orchestrator/skills/changelog-writing.md +16 -0
  20. package/orchestrator/skills/ci-pipeline-design.md +16 -0
  21. package/orchestrator/skills/cli-design.md +16 -0
  22. package/orchestrator/skills/code-review.md +27 -0
  23. package/orchestrator/skills/configuration-management.md +16 -0
  24. package/orchestrator/skills/container-review.md +16 -0
  25. package/orchestrator/skills/contract-testing.md +16 -0
  26. package/orchestrator/skills/dashboard-design.md +16 -0
  27. package/orchestrator/skills/database-migration.md +16 -0
  28. package/orchestrator/skills/database-schema-design.md +16 -0
  29. package/orchestrator/skills/debugging.md +26 -0
  30. package/orchestrator/skills/dependency-review.md +16 -0
  31. package/orchestrator/skills/design-review.md +29 -0
  32. package/orchestrator/skills/design-system.md +16 -0
  33. package/orchestrator/skills/docs-api-reference.md +16 -0
  34. package/orchestrator/skills/docs-troubleshooting.md +16 -0
  35. package/orchestrator/skills/docs-tutorial.md +16 -0
  36. package/orchestrator/skills/docs-writing.md +25 -0
  37. package/orchestrator/skills/empty-error-loading-states.md +16 -0
  38. package/orchestrator/skills/end-to-end-testing.md +16 -0
  39. package/orchestrator/skills/error-handling.md +16 -0
  40. package/orchestrator/skills/frontend-design.md +28 -0
  41. package/orchestrator/skills/git-commit-writing.md +16 -0
  42. package/orchestrator/skills/graphql-design.md +16 -0
  43. package/orchestrator/skills/incident-runbook.md +18 -0
  44. package/orchestrator/skills/input-validation.md +16 -0
  45. package/orchestrator/skills/integration-testing.md +16 -0
  46. package/orchestrator/skills/interaction-design.md +16 -0
  47. package/orchestrator/skills/landing-page-design.md +16 -0
  48. package/orchestrator/skills/observability-design.md +16 -0
  49. package/orchestrator/skills/openapi-contract.md +16 -0
  50. package/orchestrator/skills/performance-profiling.md +16 -0
  51. package/orchestrator/skills/privacy-review.md +16 -0
  52. package/orchestrator/skills/property-based-testing.md +16 -0
  53. package/orchestrator/skills/pull-request-writing.md +16 -0
  54. package/orchestrator/skills/refactoring.md +16 -0
  55. package/orchestrator/skills/release-readiness.md +16 -0
  56. package/orchestrator/skills/responsive-design.md +16 -0
  57. package/orchestrator/skills/secrets-management.md +16 -0
  58. package/orchestrator/skills/secure-file-upload.md +16 -0
  59. package/orchestrator/skills/security-review.md +26 -0
  60. package/orchestrator/skills/semantic-versioning.md +17 -0
  61. package/orchestrator/skills/seo-on-page.md +16 -0
  62. package/orchestrator/skills/seo-structured-data.md +16 -0
  63. package/orchestrator/skills/seo-technical-audit.md +16 -0
  64. package/orchestrator/skills/sql-query-review.md +16 -0
  65. package/orchestrator/skills/supply-chain-security.md +16 -0
  66. package/orchestrator/skills/test-strategy.md +16 -0
  67. package/orchestrator/skills/threat-modeling.md +16 -0
  68. package/orchestrator/skills/unit-testing.md +16 -0
  69. package/orchestrator/skills/write-tests.md +28 -0
  70. package/orchestrator/src/checkpoints.js +187 -0
  71. package/orchestrator/src/cli-registry.js +579 -0
  72. package/orchestrator/src/cli.js +135 -0
  73. package/orchestrator/src/doctor.js +64 -0
  74. package/orchestrator/src/engine.js +1364 -0
  75. package/orchestrator/src/schedule.js +126 -0
  76. package/orchestrator/src/server.js +706 -0
  77. package/orchestrator/src/skill-registry.js +272 -0
  78. package/orchestrator/src/store.js +364 -0
  79. package/orchestrator/web/OrbitControls.js +1417 -0
  80. package/orchestrator/web/app.css +116 -0
  81. package/orchestrator/web/app.js +70 -0
  82. package/orchestrator/web/board.html +141 -0
  83. package/orchestrator/web/code.html +208 -0
  84. package/orchestrator/web/flow.html +736 -0
  85. package/orchestrator/web/index.html +539 -0
  86. package/orchestrator/web/jsm/postprocessing/EffectComposer.js +231 -0
  87. package/orchestrator/web/jsm/postprocessing/MaskPass.js +104 -0
  88. package/orchestrator/web/jsm/postprocessing/Pass.js +95 -0
  89. package/orchestrator/web/jsm/postprocessing/RenderPass.js +99 -0
  90. package/orchestrator/web/jsm/postprocessing/ShaderPass.js +77 -0
  91. package/orchestrator/web/jsm/postprocessing/UnrealBloomPass.js +415 -0
  92. package/orchestrator/web/jsm/shaders/CopyShader.js +45 -0
  93. package/orchestrator/web/jsm/shaders/LuminosityHighPassShader.js +66 -0
  94. package/orchestrator/web/three.module.min.js +6 -0
  95. package/package.json +51 -0
@@ -0,0 +1,706 @@
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
+
77
+ function broadcastSchedules() {
78
+ broadcast("schedules", store.loadConfig().schedules || []);
79
+ }
80
+
81
+ function snapshot() {
82
+ return {
83
+ pending: store.listTasks("pending"),
84
+ approval: store.listTasks("approval"),
85
+ done: store.listTasks("done").filter((task) => task.kind !== "operator-chat").slice(-30).reverse(),
86
+ failed: store.listTasks("failed").slice(-30).reverse(),
87
+ };
88
+ }
89
+
90
+ function newScheduleId() {
91
+ return "sch-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7);
92
+ }
93
+ // Zamanlamanin operatorCli'si (verilmisse) kurulu bilinen bir CLI olmali; verilmezse gorev
94
+ // calisma aninda config operatorune duser (gorev olusturmayla ayni davranis).
95
+ function scheduleOperatorValid(name) {
96
+ if (!name) return true;
97
+ return Boolean(cliRegistry.DEFINITIONS[name]) && cliStatus.some((c) => c.id === name && c.installed);
98
+ }
99
+
100
+ function applyHealthToConfig(results) {
101
+ const cfg = store.loadConfig();
102
+ let changed = false;
103
+ const cachedResults = {};
104
+ for (const result of results) {
105
+ cachedResults[result.id] = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
106
+ for (const agent of Object.values(cfg.agents || {})) {
107
+ if ((agent.adapter || cliRegistry.adapterId(agent.cmd)) !== result.id) continue;
108
+ const next = result.health || { status: "unknown", label: "Bilinmiyor", detail: "" };
109
+ if (JSON.stringify(agent.health || {}) !== JSON.stringify(next)) {
110
+ agent.health = next;
111
+ changed = true;
112
+ }
113
+ }
114
+ }
115
+ const nextCache = { version: HEALTH_CACHE_VERSION, checkedAt: new Date().toISOString(), results: cachedResults };
116
+ if (JSON.stringify(cfg.cliHealthCache || {}) !== JSON.stringify(nextCache)) {
117
+ cfg.cliHealthCache = nextCache;
118
+ changed = true;
119
+ }
120
+ if (changed) store.saveConfig(cfg);
121
+ return cfg;
122
+ }
123
+
124
+ let healthRunning = false;
125
+ async function getCodexModels(force = false) {
126
+ if (!force && codexModelCache.models.length && Date.now() - codexModelCache.checkedAt < CODEX_MODEL_CACHE_TTL_MS) return codexModelCache.models;
127
+ const models = await cliRegistry.listCodexModels({ timeoutMs: 20000 });
128
+ codexModelCache = { checkedAt: Date.now(), models };
129
+ const cfg = store.loadConfig();
130
+ cfg.codexModelCache = { ...(cfg.codexModelCache || {}), checkedAt: new Date().toISOString(), models };
131
+ store.saveConfig(cfg);
132
+ return models;
133
+ }
134
+ async function refreshCliHealth(force = false) {
135
+ if (healthRunning) return cliStatus;
136
+ const cfg = store.loadConfig();
137
+ const cachedAt = Date.parse(cfg.cliHealthCache?.checkedAt || "");
138
+ if (!force && cfg.cliHealthCache?.version === HEALTH_CACHE_VERSION && Number.isFinite(cachedAt) && Date.now() - cachedAt < HEALTH_CACHE_TTL_MS) {
139
+ broadcast("cli-health", cliStatus);
140
+ return cliStatus;
141
+ }
142
+ healthRunning = true;
143
+ cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
144
+ broadcast("cli-health", cliStatus);
145
+ try {
146
+ cliStatus = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
147
+ applyHealthToConfig(cliStatus);
148
+ broadcast("cli-health", cliStatus);
149
+ return cliStatus;
150
+ } finally {
151
+ healthRunning = false;
152
+ }
153
+ }
154
+
155
+ // ---- yardimcilar ----
156
+ function send(res, code, obj) {
157
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
158
+ res.end(JSON.stringify(obj));
159
+ }
160
+ function readBody(req) {
161
+ return new Promise((resolve) => {
162
+ let b = "";
163
+ req.on("data", (c) => (b += c));
164
+ req.on("end", () => {
165
+ try {
166
+ resolve(b ? JSON.parse(b) : {});
167
+ } catch {
168
+ resolve({});
169
+ }
170
+ });
171
+ });
172
+ }
173
+ const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css" };
174
+ function serveStatic(res, file) {
175
+ const p = path.resolve(WEB, file);
176
+ if ((p !== WEB && !p.startsWith(WEB + path.sep)) || !fs.existsSync(p)) {
177
+ res.writeHead(404);
178
+ return res.end("not found");
179
+ }
180
+ res.writeHead(200, { "Content-Type": MIME[path.extname(p)] || "text/plain" });
181
+ fs.createReadStream(p).pipe(res);
182
+ }
183
+
184
+ // ---- Klasor gezgini (async + timeout) ----
185
+ // ONEMLI: eskiden statSync/readdirSync kullaniliyordu. Kopuk bir AG/eslesmis surucu
186
+ // (or. dead SMB share) statSync'te saniyelerce blokladigi icin TEK is parcacikli sunucu
187
+ // tamamen kitleniyordu (Gozat "acilmiyor"/donuyor). Artik tum FS erisimi async ve her
188
+ // cagri kisa bir timeout'a yaristirilir; donuk surucu bloklamaz, atlanir.
189
+ let driveCache = { at: 0, value: [] };
190
+ function statSafe(p, ms = 700) {
191
+ return Promise.race([
192
+ fs.promises.stat(p).catch(() => null),
193
+ new Promise((resolve) => setTimeout(() => resolve(null), ms)),
194
+ ]);
195
+ }
196
+ async function uniqExistingDirs(items) {
197
+ const seen = new Set();
198
+ const out = [];
199
+ for (const item of items) {
200
+ if (!item.path || seen.has(item.path.toLowerCase())) continue;
201
+ seen.add(item.path.toLowerCase());
202
+ const st = await statSafe(item.path);
203
+ if (st && st.isDirectory()) out.push(item);
204
+ }
205
+ return out;
206
+ }
207
+ async function listDrives() {
208
+ if (Date.now() - driveCache.at < 30000) return driveCache.value.slice();
209
+ const found = new Set();
210
+ for (const d of [process.env.SystemDrive, process.env.HOMEDRIVE]) {
211
+ if (d) found.add(path.parse(d).root || `${d.replace(/[\\/]$/, "")}\\`);
212
+ }
213
+ // Tum surucu harflerini PARALEL yokla; donuk surucu 700ms sonra atlanir (bloklamaz).
214
+ const results = await Promise.all(
215
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map(async (c) => {
216
+ const d = c + ":\\";
217
+ const st = await statSafe(d, 700);
218
+ return st && st.isDirectory() ? d : null;
219
+ })
220
+ );
221
+ for (const d of results) if (d) found.add(d);
222
+ const drives = [...found].sort((a, b) => a.localeCompare(b));
223
+ driveCache = { at: Date.now(), value: drives };
224
+ return drives.slice();
225
+ }
226
+ async function explorerPlaces() {
227
+ const home = os.homedir();
228
+ const oneDrive = process.env.OneDrive || process.env.OneDriveConsumer || process.env.OneDriveCommercial;
229
+ const candidates = [
230
+ { id: "home", label: "Ana klasör", icon: "⌂", path: home },
231
+ { id: "desktop", label: "Masaüstü", icon: "▣", path: path.join(home, "Desktop") },
232
+ oneDrive && { id: "onedrive-desktop", label: "OneDrive Masaüstü", icon: "▣", path: path.join(oneDrive, "Desktop") },
233
+ { id: "documents", label: "Belgeler", icon: "▤", path: path.join(home, "Documents") },
234
+ oneDrive && { id: "onedrive-documents", label: "OneDrive Belgeler", icon: "▤", path: path.join(oneDrive, "Documents") },
235
+ { id: "downloads", label: "İndirilenler", icon: "↓", path: path.join(home, "Downloads") },
236
+ { id: "project", label: "Çalışma klasörü", icon: "◇", path: store.WORK_BASE },
237
+ ].filter(Boolean);
238
+ return uniqExistingDirs(candidates);
239
+ }
240
+ async function rootBrowse(message) {
241
+ if (process.platform === "win32") {
242
+ const drives = await listDrives();
243
+ return {
244
+ path: "",
245
+ parent: null,
246
+ dirs: drives,
247
+ drives,
248
+ entries: drives.map((drive) => ({ name: drive, path: drive, type: "drive", modifiedAt: null })),
249
+ places: await explorerPlaces(),
250
+ isRoot: true,
251
+ warning: message || "",
252
+ };
253
+ }
254
+ return browseDir("/", message);
255
+ }
256
+ async function browseDir(p, warning = "") {
257
+ try {
258
+ if (!p) return await rootBrowse(warning);
259
+ const abs = path.resolve(p);
260
+ const rootStat = await statSafe(abs, 2000);
261
+ if (!rootStat || !rootStat.isDirectory()) throw new Error("Klasör değil");
262
+ const dirents = await fs.promises.readdir(abs, { withFileTypes: true });
263
+ const wanted = dirents.filter((e) => {
264
+ let isDir = false;
265
+ try { isDir = e.isDirectory(); } catch { isDir = false; }
266
+ return isDir && !e.name.startsWith("$") && e.name !== "System Volume Information";
267
+ });
268
+ // mtime'lari PARALEL topla; donuk bir alt klasor 400ms sonra tarihsiz gecer.
269
+ const entries = await Promise.all(
270
+ wanted.map(async (e) => {
271
+ const entryPath = path.join(abs, e.name);
272
+ const st = await statSafe(entryPath, 400);
273
+ let modifiedAt = null;
274
+ if (st) { try { modifiedAt = st.mtime.toISOString(); } catch {} }
275
+ return { name: e.name, path: entryPath, type: "folder", modifiedAt };
276
+ })
277
+ );
278
+ entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
279
+ const dirs = entries.map((entry) => entry.name);
280
+ const isDriveRoot = /^[A-Za-z]:\\?$/.test(abs);
281
+ const up = path.dirname(abs);
282
+ const parent = isDriveRoot ? "" : up === abs ? null : up;
283
+ const drives = process.platform === "win32" ? await listDrives() : [];
284
+ return { path: abs, parent, dirs, drives, entries, places: await explorerPlaces(), warning };
285
+ } catch (e) {
286
+ const requested = p ? String(p) : "";
287
+ return rootBrowse(requested ? `"${requested}" açılamadı; bu bilgisayar gösteriliyor.` : e.message);
288
+ }
289
+ }
290
+
291
+ // ---- HTTP ----
292
+ const server = http.createServer(async (req, res) => {
293
+ const { pathname } = url.parse(req.url, true);
294
+
295
+ // SSE
296
+ if (pathname === "/api/events") {
297
+ res.writeHead(200, {
298
+ "Content-Type": "text/event-stream",
299
+ "Cache-Control": "no-cache",
300
+ Connection: "keep-alive",
301
+ });
302
+ res.write(`event: status\ndata: ${JSON.stringify(engine.status())}\n\n`);
303
+ res.write(`event: queue\ndata: ${JSON.stringify(snapshot())}\n\n`);
304
+ clients.add(res);
305
+ req.on("close", () => clients.delete(res));
306
+ return;
307
+ }
308
+
309
+ // API
310
+ if (pathname.startsWith("/api/")) {
311
+ try {
312
+ if (pathname === "/api/state" && req.method === "GET") {
313
+ const cfg = store.loadConfig();
314
+ return send(res, 200, {
315
+ status: engine.status(),
316
+ queue: snapshot(),
317
+ config: cfg,
318
+ roles: store.listRoles(),
319
+ skills: skillRegistry.allSkills().map((s) => ({ name: s.name, file: s.file, description: s.description, category: s.category, appliesTo: s.appliesTo, match: s.match })),
320
+ cliStatus,
321
+ schedules: cfg.schedules || [],
322
+ platform: process.platform,
323
+ workingDirAbs: path.resolve(store.WORK_BASE, cfg.workingDir || "."),
324
+ });
325
+ }
326
+ if (pathname === "/api/security/autonomous-consent" && req.method === "POST") {
327
+ const { accepted } = await readBody(req);
328
+ if (accepted !== true) return send(res, 400, { error: "Acik kabul gerekli." });
329
+ const cfg = store.loadConfig();
330
+ cfg.autonomousConsentAcceptedAt = cfg.autonomousConsentAcceptedAt || new Date().toISOString();
331
+ store.saveConfig(cfg);
332
+ return send(res, 200, { accepted: true, acceptedAt: cfg.autonomousConsentAcceptedAt });
333
+ }
334
+ if (pathname === "/api/fs" && req.method === "GET") {
335
+ return send(res, 200, await browseDir(url.parse(req.url, true).query.path));
336
+ }
337
+ if (pathname === "/api/tasks" && req.method === "POST") {
338
+ const { prompt, targetDir, operatorCli, executionMode } = await readBody(req);
339
+ if (!prompt || !prompt.trim()) return send(res, 400, { error: "prompt gerekli" });
340
+ const cfg = store.loadConfig();
341
+ // Operatör, uzman agent'lardan bağımsız bir CLI'dır (claude/codex/gemini/opencode);
342
+ // operator.md rolüyle ekibi yönetir. Belirtilmemiş/geçersizse kurulu bir CLI'ye geçilir.
343
+ const cliEntry = (name) => cliStatus.find((c) => c.id === name);
344
+ const installedCli = (name) => name && cliRegistry.DEFINITIONS[name] && cliStatus.some((c) => c.id === name && c.installed
345
+ && (name !== "opencode" || c.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)));
346
+ const usableCli = (name) => {
347
+ const entry = cliEntry(name);
348
+ return installedCli(name) && (!entry?.health || entry.health.status === "ready");
349
+ };
350
+ let selectedCli = operatorCli || cfg.operator?.cli;
351
+ if (!installedCli(selectedCli)) {
352
+ if (cliRegistry.ensureValidOperator(cfg, cliStatus)) store.saveConfig(cfg);
353
+ selectedCli = cfg.operator?.cli;
354
+ }
355
+ if (!installedCli(selectedCli)) {
356
+ 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." });
357
+ }
358
+ if (!usableCli(selectedCli)) {
359
+ const health = cliEntry(selectedCli)?.health;
360
+ 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."}` });
361
+ }
362
+ const mode = ["auto", "fast", "balanced", "deep"].includes(executionMode) ? executionMode : "auto";
363
+ const t = store.addTask(prompt.trim(), targetDir, selectedCli, mode);
364
+ engine.wake();
365
+ broadcast("queue", snapshot());
366
+ return send(res, 200, t);
367
+ }
368
+ let m;
369
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/chat$/)) && req.method === "POST") {
370
+ const found = store.findTask(m[1]);
371
+ if (!found || found.state !== "done" || found.task.kind === "operator-chat") return send(res, 404, { error: "tamamlanmis gorev bulunamadi" });
372
+ const { question } = await readBody(req);
373
+ if (!question || !String(question).trim()) return send(res, 400, { error: "soru gerekli" });
374
+ const chatTask = store.addChatTask(found.task, String(question).trim());
375
+ if (!engine.status().running) engine.start();
376
+ else engine.wake();
377
+ broadcast("queue", snapshot());
378
+ return send(res, 200, chatTask);
379
+ }
380
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/(approve|reject)$/)) && req.method === "POST") {
381
+ const found = store.findTask(m[1]);
382
+ if (!found) return send(res, 404, { error: "gorev yok" });
383
+ if (m[2] === "approve") {
384
+ try { store.approveTask(m[1]); }
385
+ catch (error) { return send(res, 409, { error: error.message }); }
386
+ } else {
387
+ try { store.rejectTask(m[1]); }
388
+ catch (error) { return send(res, 409, { error: error.message }); }
389
+ }
390
+ broadcast("queue", snapshot());
391
+ return send(res, 200, { ok: true });
392
+ }
393
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/restore$/)) && req.method === "POST") {
394
+ const found = store.findTask(m[1]);
395
+ if (!found) return send(res, 404, { error: "gorev yok" });
396
+ if (engine.status().current) return send(res, 409, { error: "Motor bir gorev calistirirken surum geri yuklenemez; once durdurun." });
397
+ if (!found.task.checkpointId) return send(res, 400, { error: "Bu gorev icin kayitli bir surum yok." });
398
+ const cfg = store.loadConfig();
399
+ const result = checkpoints.restoreCheckpoint(found.task.checkpointId, { retention: cfg.versioningRetention });
400
+ if (!result.ok) return send(res, 409, { error: result.error || "Surum geri yuklenemedi." });
401
+ 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"}.` });
402
+ broadcast("queue", snapshot());
403
+ return send(res, 200, result);
404
+ }
405
+ if (pathname === "/api/checkpoints" && req.method === "GET") {
406
+ const dir = url.parse(req.url, true).query.dir;
407
+ return send(res, 200, checkpoints.listCheckpoints(dir ? path.resolve(store.WORK_BASE, dir) : undefined));
408
+ }
409
+ if (pathname === "/api/cli/discover" && req.method === "POST") {
410
+ cliStatus = cliRegistry.discoverInstalled();
411
+ const cfg = store.loadConfig();
412
+ const body = await readBody(req);
413
+ if (Array.isArray(body.ignoredAdapters)) {
414
+ cfg.discoveryIgnoredAdapters = [...new Set(body.ignoredAdapters.filter((id) => cliRegistry.KNOWN_CLIS.includes(id)))];
415
+ }
416
+ let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
417
+ if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
418
+ if (changed || Array.isArray(body.ignoredAdapters)) store.saveConfig(cfg);
419
+ await refreshCliHealth(true);
420
+ return send(res, 200, { cliStatus, changed, config: store.loadConfig() });
421
+ }
422
+ if (pathname === "/api/cli/health" && req.method === "POST") {
423
+ await refreshCliHealth(true);
424
+ return send(res, 200, { cliStatus, config: store.loadConfig() });
425
+ }
426
+ if (pathname === "/api/codex/models" && req.method === "GET") {
427
+ try { return send(res, 200, { models: await getCodexModels(false) }); }
428
+ catch (error) { return send(res, 502, { error: `Codex modelleri alınamadı: ${error.message}` }); }
429
+ }
430
+ if (pathname === "/api/schedules" && req.method === "GET") {
431
+ return send(res, 200, { schedules: store.loadConfig().schedules || [] });
432
+ }
433
+ if (pathname === "/api/schedules" && req.method === "POST") {
434
+ const body = await readBody(req);
435
+ let normalized;
436
+ try { normalized = schedule.normalizeSchedule(body); }
437
+ catch (error) { return send(res, 400, { error: error.message }); }
438
+ if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
439
+ normalized.id = newScheduleId();
440
+ normalized.createdAt = new Date().toISOString();
441
+ const next = schedule.computeNextRun(normalized);
442
+ if (next) normalized.nextRunAt = next.toISOString();
443
+ const cfg = store.loadConfig();
444
+ cfg.schedules = [...(cfg.schedules || []), normalized];
445
+ store.saveConfig(cfg);
446
+ broadcastSchedules();
447
+ return send(res, 200, normalized);
448
+ }
449
+ if ((m = pathname.match(/^\/api\/schedules\/([^/]+)$/)) && (req.method === "PUT" || req.method === "DELETE")) {
450
+ const cfg = store.loadConfig();
451
+ const list = cfg.schedules || [];
452
+ const index = list.findIndex((s) => s.id === m[1]);
453
+ if (index < 0) return send(res, 404, { error: "zamanlama yok" });
454
+ if (req.method === "DELETE") {
455
+ cfg.schedules = list.filter((s) => s.id !== m[1]);
456
+ store.saveConfig(cfg);
457
+ broadcastSchedules();
458
+ return send(res, 200, { ok: true });
459
+ }
460
+ const body = await readBody(req);
461
+ const current = list[index];
462
+ // Yalnizca enabled toggle'i mi, yoksa tam guncelleme mi? Alanlari mevcutla harmanla.
463
+ const merged = { ...current, ...body, id: current.id, createdAt: current.createdAt };
464
+ let normalized;
465
+ try { normalized = schedule.normalizeSchedule(merged); }
466
+ catch (error) { return send(res, 400, { error: error.message }); }
467
+ if (!scheduleOperatorValid(normalized.operatorCli)) return send(res, 400, { error: "gecersiz veya kurulu olmayan operator CLI" });
468
+ normalized.id = current.id;
469
+ normalized.createdAt = current.createdAt;
470
+ if (current.lastRunAt) normalized.lastRunAt = current.lastRunAt;
471
+ if (current.lastTaskId) normalized.lastTaskId = current.lastTaskId;
472
+ const next = normalized.enabled === false ? null : schedule.computeNextRun(normalized);
473
+ if (next) normalized.nextRunAt = next.toISOString();
474
+ else delete normalized.nextRunAt;
475
+ cfg.schedules = list.map((s, i) => (i === index ? normalized : s));
476
+ store.saveConfig(cfg);
477
+ broadcastSchedules();
478
+ return send(res, 200, normalized);
479
+ }
480
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)\/events$/)) && req.method === "GET") {
481
+ const limit = Number(url.parse(req.url, true).query.limit) || 1000;
482
+ return send(res, 200, store.listRunEvents(m[1], Math.min(limit, 5000)));
483
+ }
484
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "DELETE") {
485
+ const found = store.findTask(m[1]);
486
+ if (!found) return send(res, 404, { error: "gorev yok" });
487
+ if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev silinemez" });
488
+ store.removeTask(found.state, found.task.id);
489
+ broadcast("queue", snapshot());
490
+ return send(res, 200, { ok: true });
491
+ }
492
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "PUT") {
493
+ const found = store.findTask(m[1]);
494
+ if (!found) return send(res, 404, { error: "gorev yok" });
495
+ if (found.state !== "pending") return send(res, 409, { error: "yalnizca bekleyen gorev guncellenebilir" });
496
+ if (engine.status().current?.id === found.task.id) return send(res, 409, { error: "su an calisan gorev guncellenemez" });
497
+ const body = await readBody(req);
498
+ const cfg = store.loadConfig();
499
+ const t = found.task;
500
+ if (typeof body.prompt === "string") {
501
+ if (!body.prompt.trim()) return send(res, 400, { error: "prompt bos olamaz" });
502
+ t.prompt = body.prompt.trim();
503
+ }
504
+ if (body.executionMode && ["auto", "fast", "balanced", "deep"].includes(body.executionMode)) t.executionMode = body.executionMode;
505
+ if (body.operatorCli) {
506
+ 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" });
507
+ t.operatorCli = body.operatorCli;
508
+ }
509
+ if (typeof body.targetDir === "string") {
510
+ if (body.targetDir.trim()) t.targetDir = body.targetDir.trim();
511
+ else delete t.targetDir;
512
+ }
513
+ // Plan yeniden uretilsin: kullanici hedefi degistirdiyse eski plan gecersizdir.
514
+ delete t.teamState; delete t.planPreview; delete t.planHash; delete t.approved;
515
+ store.saveTask("pending", t);
516
+ broadcast("queue", snapshot());
517
+ return send(res, 200, t);
518
+ }
519
+ if ((m = pathname.match(/^\/api\/tasks\/([^/]+)$/)) && req.method === "GET") {
520
+ const found = store.findTask(m[1]);
521
+ return found ? send(res, 200, found) : send(res, 404, { error: "gorev yok" });
522
+ }
523
+ if (pathname === "/api/engine" && req.method === "POST") {
524
+ const { action, mode } = await readBody(req);
525
+ if (mode) engine.setMode(mode);
526
+ if (action === "start") {
527
+ if (!store.loadConfig().autonomousConsentAcceptedAt) return send(res, 409, { error: "Otonom CLI kosullarini once onaylayin.", code: "AUTONOMOUS_CONSENT_REQUIRED" });
528
+ engine.start();
529
+ }
530
+ if (action === "stop") engine.stop();
531
+ return send(res, 200, engine.status());
532
+ }
533
+ if (pathname === "/api/config" && req.method === "GET") {
534
+ return send(res, 200, store.loadConfig());
535
+ }
536
+ if (pathname === "/api/config" && req.method === "PUT") {
537
+ let cfg = await readBody(req);
538
+ if (!cfg.agents || typeof cfg.agents !== "object") return send(res, 400, { error: "agents nesnesi gerekli" });
539
+ if (Object.keys(cfg.agents).length < 1) return send(res, 400, { error: "operatorun altinda calisacak en az bir uzman agent gerekli" });
540
+ 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)" });
541
+ cfg.operator.roleFile = "roles/operator.md";
542
+ delete cfg.operator.agent;
543
+ // Eski UI govdeleri (operator.codexSettings / operator.model) yeni cliSettings
544
+ // yapisina tasinir; dogrulama tek semada yapilir.
545
+ cfg = store.normalizeConfig(cfg);
546
+ cliRegistry.normalizeAgentAdapters(cfg);
547
+ 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" });
548
+ 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" });
549
+ for (const id of ["codex", "opencode"]) {
550
+ if (cfg.cliSettings[id].model != null && typeof cfg.cliSettings[id].model !== "string") return send(res, 400, { error: `cliSettings.${id}.model metin olmali` });
551
+ }
552
+ if (cfg.skills !== undefined) {
553
+ if (typeof cfg.skills !== "object" || cfg.skills === null) return send(res, 400, { error: "skills nesne olmali" });
554
+ 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" });
555
+ 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" });
556
+ 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" });
557
+ for (const key of ["catalogLimit", "maxSkillsPerAssignment"]) {
558
+ 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` });
559
+ }
560
+ if (cfg.skills.autoMatch !== undefined && typeof cfg.skills.autoMatch !== "boolean") return send(res, 400, { error: "skills.autoMatch boolean olmali" });
561
+ }
562
+ for (const [name, agent] of Object.entries(cfg.agents)) {
563
+ if (!name.trim() || !agent.cmd || typeof agent.cmd !== "string") return send(res, 400, { error: `gecersiz agent: ${name}` });
564
+ if (!Array.isArray(agent.args)) return send(res, 400, { error: `${name}.args dizi olmali` });
565
+ }
566
+ store.saveConfig(cfg);
567
+ broadcast("status", engine.status());
568
+ return send(res, 200, { ok: true });
569
+ }
570
+ if (pathname === "/api/skills" && req.method === "GET") {
571
+ 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 })) });
572
+ }
573
+ if ((m = pathname.match(/^\/api\/skills\/(.+)$/))) {
574
+ let file = decodeURIComponent(m[1]);
575
+ if (!file.endsWith(".md")) file += ".md";
576
+ if (path.basename(file) !== file) return send(res, 400, { error: "gecersiz beceri adi" });
577
+ if (req.method === "GET") return send(res, 200, { file, content: skillRegistry.readRaw(file) });
578
+ if (req.method === "PUT") {
579
+ const { content } = await readBody(req);
580
+ const validation = skillRegistry.validateSkill(file, content || "");
581
+ if (!validation.ok) return send(res, 400, { error: validation.errors.join("; ") });
582
+ const saved = skillRegistry.writeSkill(file, content);
583
+ return send(res, 200, { ok: true, file: saved });
584
+ }
585
+ if (req.method === "DELETE") {
586
+ // Silmeden once beceri adini coz; etkin liste dosya adini degil frontmatter `name`i tutar.
587
+ const existing = skillRegistry.loadSkill(file);
588
+ const names = new Set([file.replace(/\.md$/i, ""), existing?.name].filter(Boolean));
589
+ skillRegistry.deleteSkill(file);
590
+ const cfg = store.loadConfig();
591
+ if (Array.isArray(cfg.skills?.enabled) && cfg.skills.enabled.some((name) => names.has(name))) {
592
+ cfg.skills.enabled = cfg.skills.enabled.filter((name) => !names.has(name));
593
+ store.saveConfig(cfg);
594
+ }
595
+ return send(res, 200, { ok: true });
596
+ }
597
+ }
598
+ if ((m = pathname.match(/^\/api\/roles\/(.+)$/))) {
599
+ let file = decodeURIComponent(m[1]);
600
+ if (!file.endsWith(".md")) file += ".md";
601
+ if (req.method === "GET") return send(res, 200, { file, content: store.readRole(file) });
602
+ if (req.method === "PUT") {
603
+ const { content } = await readBody(req);
604
+ store.writeRole(file, content || "");
605
+ return send(res, 200, { ok: true, file });
606
+ }
607
+ if (req.method === "DELETE") {
608
+ store.deleteRole(file);
609
+ return send(res, 200, { ok: true });
610
+ }
611
+ }
612
+ return send(res, 404, { error: "bilinmeyen endpoint" });
613
+ } catch (e) {
614
+ return send(res, 500, { error: e.message });
615
+ }
616
+ }
617
+
618
+ // Statik
619
+ serveStatic(res, pathname === "/" ? "index.html" : pathname.slice(1));
620
+ });
621
+
622
+ function openBrowser(target) {
623
+ if (process.env.OPEN === "0" || process.env.NO_OPEN) return;
624
+ const { spawn } = require("child_process");
625
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
626
+ const args = process.platform === "win32" ? ["/c", "start", "", target] : [target];
627
+ try { spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }).unref(); } catch {}
628
+ }
629
+
630
+ function startupBanner() {
631
+ const G = "\x1b[32m", Y = "\x1b[33m", DIM = "\x1b[90m", B = "\x1b[1m", C = "\x1b[36m", X = "\x1b[0m";
632
+ const installed = cliStatus.filter((c) => c.installed);
633
+ const cfg = store.loadConfig();
634
+ const url = `http://${HOST}:${PORT}`;
635
+ console.log(`\n ${B}${C}CrewCtl${X}`);
636
+ console.log(` ${DIM}operatör-liderliğinde çok-agent orkestratörü${X}\n`);
637
+ const cliLine = cliStatus.map((c) => `${c.installed ? G + "●" : DIM + "○"} ${c.id}${X}`).join(" ");
638
+ console.log(` CLI: ${cliLine}`);
639
+ console.log(` Operatör: ${cfg.operator?.cli || DIM + "(seçilemedi)" + X} ${DIM}·${X} Uzman: ${Object.values(cfg.agents).filter((a) => a.enabled !== false).length}`);
640
+ if (!installed.length) {
641
+ console.log(`\n ${Y}⚠ Hiçbir CLI kurulu değil.${X} Codex / Claude / Gemini / OpenCode'dan en az birini kurun.`);
642
+ console.log(` ${DIM}Ayrıntı için: npm run doctor${X}`);
643
+ }
644
+ console.log(`\n ${B}▶ Panel: ${C}${url}${X}`);
645
+ 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}\n`);
646
+ openBrowser(url);
647
+ }
648
+
649
+ // ---- Zamanlanmis gorev tik'i ----
650
+ // Zamani gelen zamanlamalari kuyruga alir. Salt hesap schedule.js'te; burada yalnizca yan
651
+ // etkiler var. Motor duruyorsa gorev yalnizca 'pending'e girer (manuel ekleme davranisi);
652
+ // otomatik baslatma yoktur. engine.wake() yalnizca calisan dongyu erkenden uyandirir.
653
+ const SCHEDULER_TICK_MS = 30000;
654
+ let schedulerRunning = false;
655
+ function schedulerTick() {
656
+ if (schedulerRunning) return;
657
+ schedulerRunning = true;
658
+ try {
659
+ const cfg = store.loadConfig();
660
+ const now = new Date();
661
+ const due = schedule.dueSchedules(cfg.schedules || [], now);
662
+ if (!due.length) return;
663
+ let firedAny = false;
664
+ for (const item of cfg.schedules || []) {
665
+ if (!due.some((d) => d.id === item.id)) continue;
666
+ try {
667
+ const task = store.addScheduledTask(item);
668
+ item.lastRunAt = now.toISOString();
669
+ item.lastTaskId = task.id;
670
+ const next = item.enabled === false ? null : schedule.computeNextRun(item, now);
671
+ if (next) item.nextRunAt = next.toISOString();
672
+ else delete item.nextRunAt;
673
+ firedAny = true;
674
+ 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)}` });
675
+ } catch (error) {
676
+ console.error("Zamanlanmis gorev olusturulamadi:", error.message);
677
+ }
678
+ }
679
+ if (firedAny) {
680
+ store.saveConfig(cfg);
681
+ broadcast("queue", snapshot());
682
+ broadcastSchedules();
683
+ engine.wake();
684
+ }
685
+ } catch (error) {
686
+ console.error("Zamanlayici tik hatasi:", error.message);
687
+ } finally {
688
+ schedulerRunning = false;
689
+ }
690
+ }
691
+ const schedulerTimer = setInterval(schedulerTick, SCHEDULER_TICK_MS);
692
+ if (schedulerTimer && typeof schedulerTimer.unref === "function") schedulerTimer.unref();
693
+
694
+ server.listen(PORT, HOST, startupBanner);
695
+ setImmediate(() => refreshCliHealth(false).catch((err) => console.error("CLI sağlık testi başlatılamadı:", err.message)));
696
+ setTimeout(schedulerTick, 5000).unref?.();
697
+ setImmediate(() => {
698
+ if (codexModelRefreshDue) getCodexModels(true).catch((err) => console.error("Codex model kataloğu yenilenemedi:", err.message));
699
+ });
700
+ server.on("error", (err) => {
701
+ if (err.code === "EADDRINUSE") {
702
+ console.error(`\n \x1b[31m✗ ${HOST}:${PORT} kullanımda.\x1b[0m Farklı port ile deneyin: \x1b[1mPORT=4318 npm start\x1b[0m\n`);
703
+ process.exit(1);
704
+ }
705
+ throw err;
706
+ });