@omerrgocmen/crewctl 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,8 +83,18 @@ function terminateProbeTree(child) {
83
83
  // Motor bir goreve baslarken cagirir: uctaki tum canli probe'lari sonlandirir ki operatörun
84
84
  // CLI'si (codex vb.) temiz bir alanda calissin. Probe'lar sonuc olarak "timeout/failed" doner;
85
85
  // bu zararsizdir, motor bosaldiginda yeniden denenir.
86
+ // Iptal sayaci: motor bir goreve baslarken probe'lari oldurdugunde artar. Oldurulen probe
87
+ // "cikis kodu != 0" ile doner ve saglik siniflandiricisi bunu GERCEK bir ariza sanardi;
88
+ // sonuc 6 saatlik onbellege yazilinca CLI o sure boyunca yanlislikla kullanilamaz kalirdi.
89
+ // refreshCliHealth bu sayaci once/sonra karsilastirip kirlenmis sonucu ONBELLEGE YAZMAZ.
90
+ let probeGeneration = 0;
91
+ function currentProbeGeneration() {
92
+ return probeGeneration;
93
+ }
94
+
86
95
  function abortActiveProbes() {
87
96
  const count = ACTIVE_PROBES.size;
97
+ if (count) probeGeneration++;
88
98
  for (const child of ACTIVE_PROBES) terminateProbeTree(child);
89
99
  ACTIVE_PROBES.clear();
90
100
  return count;
@@ -302,8 +312,64 @@ function probe(id, definition) {
302
312
  return result;
303
313
  }
304
314
 
305
- function discoverInstalled() {
306
- return Object.entries(DEFINITIONS).map(([id, definition]) => ({ id, ...definition, ...probe(id, definition) }));
315
+ // Her acilista tum CLI'lar --version ile yoklaniyordu; olculen maliyet 4.8 saniye ve panel
316
+ // bu sure boyunca hic acilmiyordu. Onbellek YALNIZCA su kosulda kullanilir: CLI daha once
317
+ // KURULU bulunmus VE cozulmus ikili dosya HALA diskte duruyor. Bu durumda tekrar yoklamak
318
+ // zaten bilinen bir cevabi dogrulamaktan ibarettir.
319
+ //
320
+ // Bilerek onbelleklenmeyenler (dogruluk hizdan onceliklidir):
321
+ // - Daha once "kurulu degil" bulunanlar -> yeni kurulan CLI ilk acilista gorunur.
322
+ // - Diskte bulunamayan cozulmus yol -> kaldirilan CLI hemen fark edilir.
323
+ // - opencode -> model listesi/ready durumu degisebilir;
324
+ // her acilista tam yoklanir, davranisi aynen korunur.
325
+ // "Yeniden Tara" (/api/cli/discover) daima force ile cagirir ve onbellegi tumden atlar.
326
+ const DISCOVERY_CACHE_VERSION = 1;
327
+ const ALWAYS_PROBE = new Set(["opencode"]);
328
+
329
+ // probeCommand, CLI'yi PATH uzerinden buldugunda resolvedCommand'i CIPLAK AD olarak dondurur
330
+ // ("codex"), mutlak yol olarak degil. Duz fs.existsSync bu durumda daima false verir ve
331
+ // onbellek hic tutmaz. Bu yuzden ciplak adlar PATH (+ Windows'ta PATHEXT) uzerinde aranir:
332
+ // spawn maliyeti olmadan, birkac milisaniyede ve dogru sonucla.
333
+ function resolvesOnPath(command) {
334
+ if (!command) return false;
335
+ if (command.includes("/") || command.includes("\\")) return fs.existsSync(command);
336
+ const dirs = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
337
+ const exts = isWin ? String(process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
338
+ for (const dir of dirs) {
339
+ for (const ext of exts) {
340
+ try { if (fs.existsSync(path.join(dir, command + ext))) return true; } catch {}
341
+ }
342
+ }
343
+ return false;
344
+ }
345
+
346
+ function cachedProbeUsable(entry) {
347
+ return Boolean(entry && entry.installed && entry.resolvedCommand && resolvesOnPath(entry.resolvedCommand));
348
+ }
349
+
350
+ function discoverInstalled(options = {}) {
351
+ const cache = options.cache?.version === DISCOVERY_CACHE_VERSION ? options.cache.results || {} : {};
352
+ const force = options.force === true;
353
+ return Object.entries(DEFINITIONS).map(([id, definition]) => {
354
+ const cached = cache[id];
355
+ if (!force && !ALWAYS_PROBE.has(id) && cachedProbeUsable(cached)) {
356
+ RESOLVED.set(id, cached.resolvedCommand);
357
+ return { id, ...definition, installed: true, version: cached.version || "kurulu", error: "", resolvedCommand: cached.resolvedCommand, fromCache: true };
358
+ }
359
+ return { id, ...definition, ...probe(id, definition) };
360
+ });
361
+ }
362
+
363
+ // Bir sonraki acilista kullanilacak onbellek gorununu uretir. Yalnizca kurulu ve yolu
364
+ // cozulmus girdiler saklanir; digerleri zaten her acilista yeniden yoklanir.
365
+ function discoveryCacheFrom(discovered) {
366
+ const results = {};
367
+ for (const cli of discovered || []) {
368
+ if (cli.installed && cli.resolvedCommand && !ALWAYS_PROBE.has(cli.id)) {
369
+ results[cli.id] = { installed: true, version: cli.version || "kurulu", resolvedCommand: cli.resolvedCommand };
370
+ }
371
+ }
372
+ return { version: DISCOVERY_CACHE_VERSION, checkedAt: new Date().toISOString(), results };
307
373
  }
308
374
 
309
375
  // Tek kaynak: hem gercek gorev calistirici (engine.runCli) hem saglik testi ve model
@@ -613,4 +679,4 @@ function ensureValidOperator(cfg, discovered) {
613
679
  return changed;
614
680
  }
615
681
 
616
- module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, healthCheckAll, listCodexModels, abortActiveProbes, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };
682
+ module.exports = { DEFINITIONS, KNOWN_CLIS: Object.keys(DEFINITIONS), adapterId, normalizeAgentAdapter, normalizeAgentAdapters, effectiveAgent, preparePromptArgs, operatorSpec, buildCommand, agentEnvironment, discoverInstalled, discoveryCacheFrom, healthCheckAll, listCodexModels, abortActiveProbes, currentProbeGeneration, selectOpenCodeModel, parseOpenCodeModels, addMissingAgents, ensureValidOperator };
@@ -336,18 +336,57 @@ function applyExecutionPolicy(base, mode) {
336
336
  return cfg;
337
337
  }
338
338
 
339
+ const EMPTY_USAGE = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
340
+
341
+ function addUsage(target, extra) {
342
+ const sum = { ...(target || EMPTY_USAGE) };
343
+ for (const key of Object.keys(EMPTY_USAGE)) sum[key] = (sum[key] || 0) + (Number(extra?.[key]) || 0);
344
+ return sum;
345
+ }
346
+
347
+ function usageTotal(usage) {
348
+ return (Number(usage?.input) || 0) + (Number(usage?.output) || 0);
349
+ }
350
+
351
+ // OpenCode'un step_finish olayindaki token/maliyet alanlarini normalize eder. Alan adlari
352
+ // surumler arasinda degisebildigi icin eksik alan 0 sayilir; hicbir sayi yoksa null doner
353
+ // ki "veri yok" ile "sifir maliyet" ayirt edilebilsin.
354
+ function readStepUsage(event) {
355
+ const tokens = event.part?.tokens || event.tokens;
356
+ const cost = Number(event.part?.cost ?? event.cost);
357
+ if (!tokens && !Number.isFinite(cost)) return null;
358
+ return {
359
+ input: Number(tokens?.input) || 0,
360
+ output: Number(tokens?.output) || 0,
361
+ reasoning: Number(tokens?.reasoning) || 0,
362
+ cacheRead: Number(tokens?.cache?.read) || 0,
363
+ cacheWrite: Number(tokens?.cache?.write) || 0,
364
+ cost: Number.isFinite(cost) ? cost : 0,
365
+ };
366
+ }
367
+
368
+ // DIKKAT: text/error sozlesmesi degismemeli — operatorun JSON protokolu bu metni okuyor.
369
+ // usage yalnizca EK bir alandir; cikarilamadiginda null kalir ve hicbir akisi etkilemez.
339
370
  function normalizeCliOutput(agent, stdout) {
340
- if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) return { text: String(stdout || "").trim(), error: "" };
371
+ if (agent.adapter !== "opencode" || !(agent.args || []).includes("json")) {
372
+ return { text: String(stdout || "").trim(), error: "", usage: null };
373
+ }
341
374
  const texts = [], errors = [];
375
+ let usage = null;
342
376
  for (const line of String(stdout || "").split(/\r?\n/)) {
343
377
  if (!line.trim()) continue;
344
378
  try {
345
379
  const event = JSON.parse(line);
346
380
  if (event.type === "text" && event.part?.text) texts.push(String(event.part.text));
347
381
  if (event.type === "error") errors.push(String(event.error?.message || event.error || "OpenCode model/provider hatasi"));
382
+ // Bir kosumda birden fazla adim olabilir; hepsi toplanir.
383
+ if (event.type === "step_finish") {
384
+ const step = readStepUsage(event);
385
+ if (step) usage = addUsage(usage, step);
386
+ }
348
387
  } catch {}
349
388
  }
350
- return { text: texts.join("\n").trim(), error: errors.join("\n").trim() };
389
+ return { text: texts.join("\n").trim(), error: errors.join("\n").trim(), usage };
351
390
  }
352
391
 
353
392
  // Metindeki DENGELI suslu parantezli en dis JSON nesnesi adaylarini cikarir. String icindeki
@@ -652,6 +691,35 @@ class Engine extends EventEmitter {
652
691
  defaultOperator: cfg.operator?.cli || "",
653
692
  callsToday: store.getCallCount(),
654
693
  budget: cfg.dailyCallBudget,
694
+ usageToday: store.getDailyUsage(),
695
+ };
696
+ }
697
+
698
+ // KULLANIM TELEMETRISI: gorev suresince agent bazinda token/maliyet toplar. Tamamen
699
+ // additive ve hata toleranslidir — veri veremeyen CLI'lar (metin modunda calisan
700
+ // codex/claude/gemini) icin sayaclar bos kalir, hicbir akis bundan etkilenmez.
701
+ resetUsage() {
702
+ this._usage = { total: { ...EMPTY_USAGE }, byAgent: {}, calls: 0, reportingCalls: 0 };
703
+ }
704
+
705
+ recordUsage(agentName, usage) {
706
+ if (!this._usage) this.resetUsage();
707
+ this._usage.total = addUsage(this._usage.total, usage);
708
+ this._usage.byAgent[agentName] = addUsage(this._usage.byAgent[agentName], usage);
709
+ this._usage.reportingCalls++;
710
+ try { store.addDailyUsage(usage); } catch {}
711
+ }
712
+
713
+ // Gorev kapanisinda saklanacak sekil. Hicbir cagri veri vermediyse null doner ki arayuz
714
+ // "0 token" gibi yaniltici bir sayi yerine "veri yok" gosterebilsin.
715
+ usageSummary() {
716
+ const usage = this._usage;
717
+ if (!usage || !usage.reportingCalls) return null;
718
+ return {
719
+ total: usage.total,
720
+ byAgent: usage.byAgent,
721
+ calls: usage.calls,
722
+ reportingCalls: usage.reportingCalls,
655
723
  };
656
724
  }
657
725
 
@@ -792,6 +860,8 @@ class Engine extends EventEmitter {
792
860
  // Siniflandirilmis teshis gorevle birlikte saklanir; arayuz "ne oldu / ne yapmali"
793
861
  // ayrimini ham hata metnini kirparak tahmin etmek yerine buradan okur.
794
862
  task.failure = failure;
863
+ // Basarisiz gorev de para harcar; telemetri burada da saklanmali.
864
+ task.usage = this.usageSummary();
795
865
  task.finishedAt = new Date().toISOString();
796
866
  if (found) store.moveTask(found.state, "failed", task);
797
867
  this.publish("log", { level: "error", msg: `HATA (${failure.code}): ${failure.summary}` }, task.id);
@@ -835,6 +905,8 @@ class Engine extends EventEmitter {
835
905
  if (count > (cfg.dailyCallBudget || Number.MAX_SAFE_INTEGER)) {
836
906
  return reject(new Error(`Gunluk cagri butcesi asildi (${cfg.dailyCallBudget}).`));
837
907
  }
908
+ if (!this._usage) this.resetUsage();
909
+ this._usage.calls++;
838
910
 
839
911
  const callId = `${this.current?.id || "run"}-${++this.callSequence}`;
840
912
  let promptFile = "";
@@ -945,7 +1017,10 @@ class Engine extends EventEmitter {
945
1017
  this.activeChild = null;
946
1018
  if (promptFile) { try { fs.rmSync(promptFile); } catch {} }
947
1019
  const durationMs = Date.now() - started;
948
- this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
1020
+ // Cikti tek kez ayristirilir; hem kullanim telemetrisi hem metin ayni sonuctan okunur.
1021
+ const normalized = normalizeCliOutput(agent, stdout);
1022
+ if (normalized.usage) this.recordUsage(displayName, normalized.usage);
1023
+ this.publish("activity", { ...base, kind: "process.finished", exitCode: code, signal, durationMs, usage: normalized.usage, reason: silenceTimedOut ? "silence-timeout" : (timedOut ? "timeout" : null) });
949
1024
  if (silenceTimedOut) return reject(new Error(`CLI_STALLED: ${agent.cmd} ${Math.round(silenceTimeoutMs / 1000)} saniye boyunca cikti uretmedi ve otomatik durduruldu.`));
950
1025
  if (timedOut) return reject(new Error(`${agent.cmd} ${Math.round(timeoutMs / 1000)} saniyede zaman asimina ugradi${signal ? ` (signal=${signal})` : ""}. ${clip(stdout || stderr, 500)}`));
951
1026
  // Teshis icin HER IKI akis da gerekir: bazi CLI'lar (Gemini dahil) kota/oturum hatasini
@@ -954,10 +1029,9 @@ class Engine extends EventEmitter {
954
1029
  const detail = [stderr, stdout].map((stream) => String(stream || "").trim()).filter(Boolean).join("\n");
955
1030
  return reject(new Error(`${agent.cmd} cikis kodu ${code ?? "yok"}${signal ? ` (signal=${signal})` : ""}.\n${clip(detail, 2000)}`));
956
1031
  }
957
- const normalized = normalizeCliOutput(agent, stdout);
958
1032
  if (normalized.error) return reject(new Error(`OpenCode model/provider hatasi: ${clip(normalized.error, 500)}`));
959
1033
  if (!normalized.text) return reject(new Error(`${agent.cmd} kullanilabilir cikti dondurmedi.${stderr ? ` ${clip(stderr, 300)}` : ""}`));
960
- resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId });
1034
+ resolve({ text: normalized.text, stderr: stderr.trim(), exitCode: code, durationMs, callId, usage: normalized.usage });
961
1035
  });
962
1036
 
963
1037
  // Prompt argumanla/dosyayla verilse bile stdin MUTLAKA kapatilmali. OpenCode gibi CLI'lar
@@ -1206,6 +1280,8 @@ class Engine extends EventEmitter {
1206
1280
  this._cwd = path.resolve(store.WORK_BASE, task.targetDir || cfg.workingDir || ".");
1207
1281
  this._snapBefore = snapshotDir(this._cwd);
1208
1282
  this.startLiveDiff(task);
1283
+ // Onay sonrasi devam eden gorevde sayaci sifirlamayiz; ilk girise ozgu.
1284
+ if (!task.approved || !this._usage) this.resetUsage();
1209
1285
  this.publish("log", { level: "task", msg: `GOREV ${task.id}: ${task.prompt}` }, task.id);
1210
1286
  // Otomatik surumleme: gorev CALISMADAN ONCE calisma klasorunun bir surumunu al ki ajan
1211
1287
  // mevcut kodu bozarsa bu gorev oncesine tek tikla donulebilsin. Yalnizca ilk girus'te
@@ -1504,6 +1580,7 @@ class Engine extends EventEmitter {
1504
1580
  task.summary = concise;
1505
1581
  task.final = finalText;
1506
1582
  task.changes = changes;
1583
+ task.usage = this.usageSummary();
1507
1584
  const warnings = (Array.isArray(decision.warnings) ? decision.warnings : []).map(String).filter(Boolean);
1508
1585
  task.delivery = {
1509
1586
  summary: concise,
@@ -1519,7 +1596,7 @@ class Engine extends EventEmitter {
1519
1596
  const found = store.findTask(task.id);
1520
1597
  store.moveTask(found?.state || "pending", status, task);
1521
1598
  store.appendMemory(`${task.id} [${status}] ${task.prompt}`, `${task.summary}\nDosyalar: ${[...changes.created, ...changes.modified, ...changes.deleted].join(", ")}`);
1522
- this.publish("result", { id: task.id, prompt: task.prompt, status, dir: this._cwd, changes, summary: task.summary, delivery: task.delivery, operator: task.operatorCli, rounds: state.round }, task.id);
1599
+ this.publish("result", { id: task.id, prompt: task.prompt, status, dir: this._cwd, changes, summary: task.summary, delivery: task.delivery, usage: task.usage, operator: task.operatorCli, rounds: state.round }, task.id);
1523
1600
  this.publish("log", { level: "ok", msg: `GOREV tamamlandi: ${task.id}` }, task.id);
1524
1601
  this.notifyOutcome(task, status, { summary: task.summary, rounds: state.round, fileCount: files.length, verification: task.delivery.verification });
1525
1602
  this.emit("queue");
@@ -1549,4 +1626,5 @@ module.exports._internals = {
1549
1626
  normalizeAssignments, ensureBalancedRoleChain, extractVerdict, clipMiddle, snapshotDir, diffSnapshots,
1550
1627
  captureTextSnapshot, buildLineDiff, describeFileDiff, isSensitiveDiffPath, taskRequiresDelegation,
1551
1628
  parseJson, conversationalAnswer, classifyCliError, RECOVERABLE_CLI_ERRORS, QUARANTINE_CLI_ERRORS,
1629
+ normalizeCliOutput, addUsage, usageTotal,
1552
1630
  };
@@ -30,9 +30,14 @@ let codexModelRefreshDue = false;
30
30
  let codexModelCache = { checkedAt: 0, models: [] };
31
31
 
32
32
  store.ensureDirs();
33
- let cliStatus = cliRegistry.discoverInstalled();
33
+ // Acilis maliyetini dusurmek icin onceki kesfin sonucu kullanilir; ayrintili kurallar
34
+ // cli-registry.discoverInstalled icinde. Onbellek gecersiz/eksikse tam yoklama yapilir.
35
+ let cliStatus = cliRegistry.discoverInstalled({ cache: store.loadConfig().cliDiscoveryCache });
34
36
  {
35
37
  const cfg = store.loadConfig();
38
+ const nextDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
39
+ const discoveryCacheChanged = JSON.stringify((cfg.cliDiscoveryCache || {}).results || {}) !== JSON.stringify(nextDiscoveryCache.results);
40
+ if (discoveryCacheChanged) cfg.cliDiscoveryCache = nextDiscoveryCache;
36
41
  const savedModels = cfg.codexModelCache && typeof cfg.codexModelCache === "object" ? cfg.codexModelCache : {};
37
42
  const startupCount = Number(savedModels.startupCount || 0) + 1;
38
43
  codexModelCache = {
@@ -53,7 +58,7 @@ let cliStatus = cliRegistry.discoverInstalled();
53
58
  }
54
59
  let changed = cliRegistry.addMissingAgents(cfg, cliStatus);
55
60
  if (cliRegistry.ensureValidOperator(cfg, cliStatus)) changed = true;
56
- if (changed || staleHealthCleared || JSON.stringify(savedModels) !== JSON.stringify(cfg.codexModelCache)) store.saveConfig(cfg);
61
+ if (changed || staleHealthCleared || discoveryCacheChanged || JSON.stringify(savedModels) !== JSON.stringify(cfg.codexModelCache)) store.saveConfig(cfg);
57
62
  }
58
63
 
59
64
  // ---- SSE istemcileri ----
@@ -191,8 +196,22 @@ async function refreshCliHealth(force = false) {
191
196
  healthRunning = true;
192
197
  cliStatus = cliStatus.map((cli) => ({ ...cli, health: { status: "testing", label: "Test ediliyor", detail: "Gerçek CLI sağlık testi çalışıyor." } }));
193
198
  broadcast("cli-health", cliStatus);
199
+ const generation = cliRegistry.currentProbeGeneration();
194
200
  try {
195
- cliStatus = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
201
+ const results = await cliRegistry.healthCheckAll(cliStatus, { timeoutMs: 45000, cfg });
202
+ // Motor bu sirada bir goreve basladiysa probe'lar SIGKILL edilmistir; elimizdeki
203
+ // "failed/timeout" sonuclari gercek degil, iptalin yan urunudur. Onbellege yazmak
204
+ // CLI'yi 6 saat boyunca yanlislikla arizali gosterirdi. Durumu bilinmiyor'a cekip
205
+ // (engellemeyen gecici durum) bir sonraki tetiklemede yeniden olcmeyi bekleriz.
206
+ if (cliRegistry.currentProbeGeneration() !== generation) {
207
+ cliStatus = cliStatus.map((cli) => ({
208
+ ...cli,
209
+ health: { status: "unknown", label: "Bilinmiyor", detail: "Sağlık testi, bir görev başladığı için yarıda kesildi; sonuç kaydedilmedi." },
210
+ }));
211
+ broadcast("cli-health", cliStatus);
212
+ return cliStatus;
213
+ }
214
+ cliStatus = results;
196
215
  applyHealthToConfig(cliStatus);
197
216
  broadcast("cli-health", cliStatus);
198
217
  return cliStatus;
@@ -392,9 +411,17 @@ const server = http.createServer(async (req, res) => {
392
411
  const cliEntry = (name) => cliStatus.find((c) => c.id === name);
393
412
  const installedCli = (name) => name && cliRegistry.DEFINITIONS[name] && cliStatus.some((c) => c.id === name && c.installed
394
413
  && (name !== "opencode" || c.ready !== false || Boolean(cfg.cliSettings?.opencode?.model)));
414
+ // "testing" ve "unknown" BILGI YOKLUGUDUR, ariza kaniti degildir. Eskiden yalnizca
415
+ // "ready" kabul ediliyordu; bu yuzden acilistaki saglik testi suresince (olculen: 13.3 sn)
416
+ // HER gorev 409 ile reddediliyor ve kullaniciya "CLI kullanilabilir degil" deniyordu.
417
+ // Bu kume kasitli olarak eski davranisin UST KUMESIDIR: bugun kabul edilen hicbir
418
+ // gorev reddedilemez, yalnizca iki gecici durum artik engellemiyor. Gercekten arizali
419
+ // CLI'lar (auth-required/quota/failed/timeout/version-incompatible) aynen engellenir.
420
+ const TRANSIENT_HEALTH = new Set(["testing", "unknown"]);
395
421
  const usableCli = (name) => {
396
- const entry = cliEntry(name);
397
- return installedCli(name) && (!entry?.health || entry.health.status === "ready");
422
+ if (!installedCli(name)) return false;
423
+ const status = cliEntry(name)?.health?.status;
424
+ return !status || status === "ready" || TRANSIENT_HEALTH.has(status);
398
425
  };
399
426
  let selectedCli = operatorCli || cfg.operator?.cli;
400
427
  if (!installedCli(selectedCli)) {
@@ -456,8 +483,10 @@ const server = http.createServer(async (req, res) => {
456
483
  return send(res, 200, checkpoints.listCheckpoints(dir ? path.resolve(store.WORK_BASE, dir) : undefined));
457
484
  }
458
485
  if (pathname === "/api/cli/discover" && req.method === "POST") {
459
- cliStatus = cliRegistry.discoverInstalled();
486
+ // "Yeniden Tara" kullanicinin acik talebidir: onbellegi tumden atla, her CLI'yi yokla.
487
+ cliStatus = cliRegistry.discoverInstalled({ force: true });
460
488
  const cfg = store.loadConfig();
489
+ cfg.cliDiscoveryCache = cliRegistry.discoveryCacheFrom(cliStatus);
461
490
  const body = await readBody(req);
462
491
  if (Array.isArray(body.ignoredAdapters)) {
463
492
  cfg.discoveryIgnoredAdapters = [...new Set(body.ignoredAdapters.filter((id) => cliRegistry.KNOWN_CLIS.includes(id)))];
@@ -365,6 +365,31 @@ function bumpCallCount() {
365
365
  return n;
366
366
  }
367
367
 
368
+ // ---- Gunluk token/maliyet birikimi ----
369
+ // Cagri sayacinin yanina gunluk token ve maliyet toplami. Ayri dosyada tutulur ki mevcut
370
+ // calls-*.txt bicimi (ve onu okuyan cli.js) hic degismesin. Bozuk/eksik dosya sifir sayilir;
371
+ // telemetri hicbir kosulda gorev akisini durdurmamali.
372
+ const USAGE_FIELDS = ["input", "output", "reasoning", "cacheRead", "cacheWrite", "cost"];
373
+ function usageFile(day) {
374
+ return path.join(STATE, `usage-${day || new Date().toISOString().slice(0, 10)}.json`);
375
+ }
376
+ function getDailyUsage(day) {
377
+ const empty = Object.fromEntries(USAGE_FIELDS.map((k) => [k, 0]));
378
+ try {
379
+ const parsed = JSON.parse(fs.readFileSync(usageFile(day), "utf8"));
380
+ for (const key of USAGE_FIELDS) empty[key] = Number(parsed?.[key]) || 0;
381
+ empty.calls = Number(parsed?.calls) || 0;
382
+ } catch { empty.calls = 0; }
383
+ return empty;
384
+ }
385
+ function addDailyUsage(usage) {
386
+ const current = getDailyUsage();
387
+ for (const key of USAGE_FIELDS) current[key] += Number(usage?.[key]) || 0;
388
+ current.calls += 1;
389
+ atomicWrite(usageFile(), JSON.stringify(current));
390
+ return current;
391
+ }
392
+
368
393
  module.exports = {
369
394
  ROOT,
370
395
  ASSETS,
@@ -393,6 +418,8 @@ module.exports = {
393
418
  appendMemory,
394
419
  getCallCount,
395
420
  bumpCallCount,
421
+ getDailyUsage,
422
+ addDailyUsage,
396
423
  appendRunEvent,
397
424
  listRunEvents,
398
425
  hashText,
@@ -113,6 +113,12 @@ details summary{cursor:pointer;color:var(--dim);font-size:12px} pre{white-space:
113
113
  .fi-log.error .fi-msg,.fi-log.warn .fi-msg{color:var(--evc)}.fi-log.ok .fi-msg,.fi-log.task .fi-msg{font-weight:600}
114
114
  /* Teslimat */
115
115
  .delivery{margin-top:8px;border-top:1px solid var(--line);padding-top:7px}
116
+ .usage-box{margin-top:8px;border:1px solid var(--line);border-radius:9px;background:color-mix(in srgb,var(--acc) 5%,transparent);padding:8px 10px}
117
+ .usage-head{display:flex;align-items:baseline;gap:8px;font-weight:700;font-size:12.5px}.usage-head .usage-cost{margin-left:auto;font-family:ui-monospace,monospace;color:var(--acc)}
118
+ .usage-detail{margin-top:3px;font-size:11px;color:var(--dim);overflow-wrap:anywhere}
119
+ .usage-agents{margin-top:6px;border-top:1px solid var(--line);padding-top:5px;display:grid;gap:2px}
120
+ .usage-agents>div{display:flex;gap:8px;font-size:11px}.usage-agents>div>span:first-child{color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.usage-agents>div>span:last-child{margin-left:auto;font-family:ui-monospace,monospace;white-space:nowrap}
121
+ .usage-note{margin-top:5px;font-size:10.5px;color:var(--warn)}
116
122
  .failure-box{margin-top:8px;border:1px solid color-mix(in srgb,var(--err) 45%,var(--line));border-radius:9px;background:color-mix(in srgb,var(--err) 8%,transparent);padding:9px 11px}
117
123
  .failure-box .fb-title{font-weight:700;font-size:12.5px;display:flex;gap:7px;align-items:baseline;flex-wrap:wrap}
118
124
  .failure-box .fb-code{font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.4px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 40%,transparent);border-radius:5px;padding:1px 5px}
@@ -245,7 +251,7 @@ details summary{cursor:pointer;color:var(--dim);font-size:12px} pre{white-space:
245
251
  <h1><span class="brand">CrewCtl</span> Command Center</h1>
246
252
  <div class="header-state"><span class="pill"><span id="dot" class="dot"></span><span id="engineState">durdu</span></span><span id="current" class="pill">boşta</span></div>
247
253
  <div class="grow"></div>
248
- <div class="header-tools"><span id="budget" class="pill">0 çağrı</span><button id="startBtn" class="enginebtn ready" onclick="engine('start')"><span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5v14l11-7z" fill="currentColor"/></svg></span> Başlat</button><button id="stopBtn" class="enginebtn idle" onclick="engine('stop')"><span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="1.6" fill="currentColor"/></svg></span> Durdur</button><button onclick="location.href='board.html'">Pano</button><button onclick="gotoFlow()">Ekip Akışı</button><button onclick="location.href='code.html'">Canlı Kod</button><button onclick="openSettings()">Ayarlar</button><button id="helpBtn" title="Bu sistem nedir?"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v5M12 7.6h.01"/></svg></button><button id="themeBtn" onclick="toggleTheme()" title="Açık / koyu tema"><svg class="i i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><svg class="i i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button></div>
254
+ <div class="header-tools"><span id="usagePill" class="pill" hidden></span><span id="budget" class="pill">0 çağrı</span><button id="startBtn" class="enginebtn ready" onclick="engine('start')"><span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5v14l11-7z" fill="currentColor"/></svg></span> Başlat</button><button id="stopBtn" class="enginebtn idle" onclick="engine('stop')"><span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="1.6" fill="currentColor"/></svg></span> Durdur</button><button onclick="location.href='board.html'">Pano</button><button onclick="gotoFlow()">Ekip Akışı</button><button onclick="location.href='code.html'">Canlı Kod</button><button onclick="openSettings()">Ayarlar</button><button id="helpBtn" title="Bu sistem nedir?"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v5M12 7.6h.01"/></svg></button><button id="themeBtn" onclick="toggleTheme()" title="Açık / koyu tema"><svg class="i i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><svg class="i i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button></div>
249
255
  </header>
250
256
  <main>
251
257
  <aside class="stack">
@@ -391,7 +397,12 @@ function operatorCliOptions(includeUnready=false){return installedClis(includeUn
391
397
  function fillOperatorSelect(el,selectedCli){if(!el)return;const opts=operatorCliOptions();const cur=opts.some(o=>o.cli===selectedCli)?selectedCli:opts[0]?.cli;el.innerHTML=opts.map(o=>`<option value="${o.cli}" ${o.cli===cur?'selected':''}>${esc(o.label)}</option>`).join('')}
392
398
  // sync=true → görev formunu ayarlardaki varsayılan operatör CLI'sına eşitle
393
399
  function refreshOperatorSelect(sync){const el=$('#taskOperator');if(!el)return;fillOperatorSelect(el,sync?STATE.config.operator?.cli:(el.value||STATE.config.operator?.cli))}
394
- function applyStatus(s){STATE.status=s;$('#dot').className='dot'+(s.running?' on':'');$('#engineState').textContent=s.running?'çalışıyor':'durdu';$('#current').textContent=s.current?`${s.current.stage} · ${s.current.agent}`:(s.busy?'meşgul':'boşta');$('#budget').textContent=`${s.callsToday}/${s.budget} çağrı`;renderEngineButtons(s.running)}
400
+ /* Token/maliyet biçimleyicileri. Yalnızca JSON modunda çalışan CLI'lar (bugün OpenCode) veri
401
+ üretir; veri yoksa sayı uydurmak yerine bölüm hiç gösterilmez. */
402
+ function fmtTokens(n){const v=Number(n)||0;if(v>=1e6)return (v/1e6).toFixed(v>=1e7?0:1)+'M';if(v>=1e3)return (v/1e3).toFixed(v>=1e4?0:1)+'B';return String(v)}
403
+ function fmtCost(n){const v=Number(n)||0;if(!v)return '$0';return v<0.01?'<$0.01':'$'+v.toFixed(v<1?3:2)}
404
+ function usageLine(u){if(!u)return '';const t=u.total||u;return `${fmtTokens((t.input||0)+(t.output||0))} token · ${fmtCost(t.cost)}`}
405
+ function applyStatus(s){STATE.status=s;$('#dot').className='dot'+(s.running?' on':'');$('#engineState').textContent=s.running?'çalışıyor':'durdu';$('#current').textContent=s.current?`${s.current.stage} · ${s.current.agent}`:(s.busy?'meşgul':'boşta');$('#budget').textContent=`${s.callsToday}/${s.budget} çağrı`;const ut=s.usageToday,tok=ut?(ut.input||0)+(ut.output||0):0,pill=$('#usagePill');if(pill){pill.hidden=!tok;pill.textContent=`${fmtTokens(tok)} token · ${fmtCost(ut&&ut.cost)} bugün`;pill.title=ut?`Bugün: ${(ut.input||0).toLocaleString('tr')} girdi + ${(ut.output||0).toLocaleString('tr')} çıktı token, ${(ut.cacheRead||0).toLocaleString('tr')} önbellekten okundu`:''}renderEngineButtons(s.running)}
395
406
  function renderEngineButtons(running){const start=$('#startBtn'),stop=$('#stopBtn');if(!start||!stop)return;if(running){start.className='enginebtn on';start.disabled=true;start.innerHTML='<span class="rdot"></span> Çalışıyor';stop.className='enginebtn live';stop.disabled=false;stop.innerHTML='<span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="1.6" fill="currentColor"/></svg></span> Durdur'}else{start.className='enginebtn ready';start.disabled=false;start.innerHTML='<span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5v14l11-7z" fill="currentColor"/></svg></span> Başlat';stop.className='enginebtn idle';stop.disabled=true;stop.innerHTML='<span class="ic"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="1.6" fill="currentColor"/></svg></span> Durdur'}}
396
407
  function showConsent(){const modal=$('#consentModal');if(modal)modal.classList.add('open')}
397
408
  function dismissConsent(){$('#consentModal').classList.remove('open')}
@@ -409,7 +420,10 @@ function showToast(kind,title,sub){let box=$('#toasts');if(!box){box=document.cr
409
420
  async function addTask(){const prompt=$('#prompt').value.trim(),operatorCli=$('#taskOperator').value,targetDir=$('#targetDir').value.trim(),executionMode=$('#executionMode').value;if(!prompt)return;ensureNotifyPermission();try{await api('/api/tasks','POST',{prompt,operatorCli,executionMode,targetDir:targetDir||undefined});$('#prompt').value='';showToast('ok','Görev kuyruğa eklendi','Operatör sıraya alındığında çalışmaya başlayacak.')}catch(e){showToast('err','Görev eklenemedi',e.message)}}
410
421
 
411
422
  /* ---- Kuyruk ---- */
412
- function deliveryHtml(t){const d=t.delivery;if(!d)return '';const labels={created:'eklendi',modified:'değişti',deleted:'silindi'};return `<div class="delivery"><div class="muted">📁 ${esc(d.location||'')}</div><div class="muted">⚡ ${(d.mode||'').toUpperCase()} · ${d.rounds||0} tur · ${(d.agents||[]).join(', ')}</div>${(d.files||[]).map(f=>`<div class="file-change"><span class="${esc(f.action)}">${esc(labels[f.action]||f.action)}</span><code>${esc(f.path)}</code></div>`).join('')||'<div class="muted">Dosya değişikliği yok.</div>'}${d.verification?`<div class="muted" style="margin-top:5px">✓ ${esc(d.verification)}</div>`:''}${(d.warnings||[]).map(w=>`<div style="margin-top:5px;color:var(--warn)">⚠ ${esc(w)}</div>`).join('')}</div>`}
423
+ function deliveryHtml(t){const d=t.delivery;if(!d)return '';const labels={created:'eklendi',modified:'değişti',deleted:'silindi'};return `<div class="delivery"><div class="muted">📁 ${esc(d.location||'')}</div><div class="muted">⚡ ${(d.mode||'').toUpperCase()} · ${d.rounds||0} tur · ${(d.agents||[]).join(', ')}</div>${(d.files||[]).map(f=>`<div class="file-change"><span class="${esc(f.action)}">${esc(labels[f.action]||f.action)}</span><code>${esc(f.path)}</code></div>`).join('')||'<div class="muted">Dosya değişikliği yok.</div>'}${d.verification?`<div class="muted" style="margin-top:5px">✓ ${esc(d.verification)}</div>`:''}${(d.warnings||[]).map(w=>`<div style="margin-top:5px;color:var(--warn)">⚠ ${esc(w)}</div>`).join('')}${usageHtml(t)}</div>`}
424
+ /* Maliyet dökümü. reportingCalls < calls ise bazı CLI'lar telemetri vermemiştir; bunu
425
+ gizlemek yerine söyleriz, yoksa kullanıcı eksik toplamı gerçek sanır. */
426
+ function usageHtml(t){const u=t.usage;if(!u||!u.total)return '';const tt=u.total,rows=Object.entries(u.byAgent||{});const missing=(u.calls||0)-(u.reportingCalls||0);return `<div class="usage-box"><div class="usage-head"><span>${fmtTokens((tt.input||0)+(tt.output||0))} token</span><span class="usage-cost">${fmtCost(tt.cost)}</span></div><div class="usage-detail">${(tt.input||0).toLocaleString('tr')} girdi · ${(tt.output||0).toLocaleString('tr')} çıktı${tt.reasoning?` · ${tt.reasoning.toLocaleString('tr')} akıl yürütme`:''}${tt.cacheRead?` · ${tt.cacheRead.toLocaleString('tr')} önbellek`:''}</div>${rows.length>1?`<div class="usage-agents">${rows.map(([name,a])=>`<div><span>${esc(name)}</span><span>${fmtTokens((a.input||0)+(a.output||0))} · ${fmtCost(a.cost)}</span></div>`).join('')}</div>`:''}${missing>0?`<div class="usage-note">${missing} çağrı telemetri bildirmedi; toplam eksik olabilir.</div>`:''}</div>`}
413
427
  /* Başarısız görev kartı: kod + ne olduğu + ne yapılacağı görünür, ham çıktı katlanmış durur. */
414
428
  function failureHtml(t){const f=t.failure||{},title=f.summary||String(t.error||'Görev tamamlanamadı');return `<div class="failure-box"><div class="fb-title">${f.code?`<span class="fb-code">${esc(f.code)}</span>`:''}<span>${esc(title)}</span></div>${f.action?`<div class="fb-action">→ ${esc(f.action)}</div>`:''}${f.raw?`<details><summary>Ham CLI çıktısı</summary><pre>${esc(f.raw)}</pre></details>`:''}</div>`}
415
429
  function taskCard(t,state){const actions=state==='approval',done=state==='done',pending=state==='pending',editable=pending&&t.kind!=='operator-chat';const opLabel=t.operatorCli?cliLabel(t.operatorCli):'';return `<div class="task"><div class="row"><span class="id">${esc(t.id)}</span><div class="grow"></div><span class="badge">${esc(t.executionMode||'auto')}</span><span class="badge">${esc(opLabel)}</span></div><div class="prompt">${esc(t.prompt)}</div>${t.summary?`<details ${(t.delivery&&(t.delivery.files||[]).length)?'':'open'}><summary>Kısa teslimat özeti</summary><pre>${esc(t.summary)}</pre>${deliveryHtml(t)}</details>`:''}${state==='failed'?failureHtml(t):''}${t.planPreview?`<details><summary>Takım planı</summary><pre>${esc(t.planPreview)}</pre></details>`:''}<div class="row" style="margin-top:6px">${actions?`<button class="ok sm" onclick="approve('${t.id}')">Onayla</button><button class="danger sm" onclick="rejectTask('${t.id}')">Reddet</button>`:`<button class="sm" onclick="inspectRun('${t.id}')">Akışı incele</button>${(done||state==='failed')&&(t.delivery&&(t.delivery.files||[]).length)?`<button class="sm" onclick="location.href='code.html?task=${t.id}'" title="Bu görevin kod değişikliklerini satır satır gör">Kodu gör</button>`:''}${(done||state==='failed')&&t.checkpointId?`<button class="danger sm" onclick="restoreVersion('${t.id}')" title="Bu görev öncesindeki kod sürümüne dön"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>Bu sürüme dön</button>`:''}${editable?`<button class="sm" onclick="openEdit('${t.id}')">Düzenle</button><button class="danger sm" onclick="deleteTask('${t.id}')">Sil</button>`:''}${done?`<button class="primary sm" onclick="openChat('${t.id}')">Operatöre Sor</button>`:''}`}</div></div>`}
@@ -465,7 +479,7 @@ const FAILURE_RULES=[
465
479
  ];
466
480
  function friendlyFailure(text,agent){for(const [re,title,body,action] of FAILURE_RULES){if(re.test(text))return {title:`${agent} ${title}`,body,action}}return {title:`${agent} görevi tamamlayamadı`,body:'CLI sıfırdan farklı çıkış koduyla kapandı.',action:'Ham çıktı aşağıdaki “CLI çalışma kaydı” bölümünde saklandı; nedeni orada görebilirsiniz.'}}
467
481
  function parseOperatorView(raw){try{let text=raw.trim(),m=text.match(/```(?:json)?\s*([\s\S]*?)```/i);if(m)text=m[1];const data=JSON.parse(text);if(Array.isArray(data.assignments)){const names=[...new Set(data.assignments.map(x=>x.agent))];return {title:data.status==='continue'?'Operatör yeni ekip turu oluşturdu':'Takım planı hazır',body:`${data.assignments.length} delegasyon · ${(data.completionCriteria||[]).length} kabul kriteri`,pills:names}}if(data.status==='complete')return {title:'Operatör görevi tamamladı',body:String(data.final||'Nihai sonuç hazır.'),pills:[]}}catch{}return null}
468
- function activity(e){const c=ensureCall(e);const role=String(e.stage||'').startsWith('operator')?'operator':'agent';if(e.kind==='stdout'||e.kind==='stderr'){const span=document.createElement('span');if(e.kind==='stderr')span.className='stderr';span.textContent=e.text;c.term.appendChild(span);c.raw+=e.text;c[e.kind]+=e.text;c.term.scrollTop=c.term.scrollHeight;c.state.textContent='çalışıyor';setTeamState(e.agent,'working','çalışıyor',role)}else if(e.kind==='process.progress'){const sec=Math.max(1,Math.round((e.elapsedMs||0)/1000));c.state.textContent=`yanıt bekleniyor · ${sec} sn`;setTeamState(e.agent,'working',`${sec} sn · yanıt bekleniyor`,role)}else if(e.kind==='process.finished'){c.box.classList.remove('active');const failed=Number(e.exitCode)!==0,stalled=e.reason==='silence-timeout';c.state.textContent=stalled?`yanıt vermedi · ${(e.durationMs/1000).toFixed(1)}s`:failed?`başarısız · ${(e.durationMs/1000).toFixed(1)}s`:`tamamlandı · ${(e.durationMs/1000).toFixed(1)}s`;setTeamState(e.agent,failed?'failed':'done',stalled?'otomatik durduruldu':failed?'erişilemiyor':'teslim etti',role);if(failed){c.box.classList.add('error');const f=friendlyFailure(stalled?'CLI_STALLED':c.raw,e.agent);c.friendly.hidden=false;c.friendly.innerHTML=`<b>${esc(f.title)}</b>${esc(f.body)}<br><small>${esc(f.action)}</small>`;c.details.open=false}else if(role==='operator'){const view=parseOperatorView(c.stdout||c.raw);if(view){c.friendly.hidden=false;c.friendly.classList.add('success');c.friendly.innerHTML=`<b>${esc(view.title)}</b>${esc(view.body)}${view.pills.length?`<div class="plan-pills">${view.pills.map(x=>`<span>${esc(x)}</span>`).join('')}</div>`:''}`;c.details.open=false}}else{const clean=String(c.stdout||'').replace(/tokens used[\s\S]*$/i,'').trim();c.friendly.hidden=false;c.friendly.classList.add('success');c.friendly.innerHTML=`<b>Teslimat operatöre gönderildi</b>${esc(clean.slice(0,420)||'Agent görevini başarıyla tamamladı.')}<br><small>Komutlar ve ayrıntılı çıktı “Canlı CLI çıktısı” bölümünde saklandı.</small>`;c.details.open=false}}else if(e.kind==='process.failed'||e.kind==='process.timeout'||e.kind==='process.silence-timeout'){c.box.classList.remove('active');c.box.classList.add('error');c.state.textContent=e.kind==='process.silence-timeout'?'yanıt vermedi · durduruldu':'hata';setTeamState(e.agent,'failed',e.kind==='process.silence-timeout'?'otomatik durduruldu':'hata',role)}else if(e.kind==='process.started'){missionCalls++;updateMissionStats();c.state.textContent='başlatıldı';setTeamState(e.agent,'working',role==='operator'?'ekibi yönetiyor':'görevi yürütüyor',role)}}
482
+ function activity(e){const c=ensureCall(e);const role=String(e.stage||'').startsWith('operator')?'operator':'agent';if(e.kind==='stdout'||e.kind==='stderr'){const span=document.createElement('span');if(e.kind==='stderr')span.className='stderr';span.textContent=e.text;c.term.appendChild(span);c.raw+=e.text;c[e.kind]+=e.text;c.term.scrollTop=c.term.scrollHeight;c.state.textContent='çalışıyor';setTeamState(e.agent,'working','çalışıyor',role)}else if(e.kind==='process.progress'){const sec=Math.max(1,Math.round((e.elapsedMs||0)/1000));c.state.textContent=`yanıt bekleniyor · ${sec} sn`;setTeamState(e.agent,'working',`${sec} sn · yanıt bekleniyor`,role)}else if(e.kind==='process.finished'){c.box.classList.remove('active');const failed=Number(e.exitCode)!==0,stalled=e.reason==='silence-timeout';const dur=(e.durationMs/1000).toFixed(1);c.state.textContent=stalled?`yanıt vermedi · ${dur}s`:failed?`başarısız · ${dur}s`:`tamamlandı · ${dur}s${e.usage?` · ${usageLine(e.usage)}`:''}`;setTeamState(e.agent,failed?'failed':'done',stalled?'otomatik durduruldu':failed?'erişilemiyor':'teslim etti',role);if(failed){c.box.classList.add('error');const f=friendlyFailure(stalled?'CLI_STALLED':c.raw,e.agent);c.friendly.hidden=false;c.friendly.innerHTML=`<b>${esc(f.title)}</b>${esc(f.body)}<br><small>${esc(f.action)}</small>`;c.details.open=false}else if(role==='operator'){const view=parseOperatorView(c.stdout||c.raw);if(view){c.friendly.hidden=false;c.friendly.classList.add('success');c.friendly.innerHTML=`<b>${esc(view.title)}</b>${esc(view.body)}${view.pills.length?`<div class="plan-pills">${view.pills.map(x=>`<span>${esc(x)}</span>`).join('')}</div>`:''}`;c.details.open=false}}else{const clean=String(c.stdout||'').replace(/tokens used[\s\S]*$/i,'').trim();c.friendly.hidden=false;c.friendly.classList.add('success');c.friendly.innerHTML=`<b>Teslimat operatöre gönderildi</b>${esc(clean.slice(0,420)||'Agent görevini başarıyla tamamladı.')}<br><small>Komutlar ve ayrıntılı çıktı “Canlı CLI çıktısı” bölümünde saklandı.</small>`;c.details.open=false}}else if(e.kind==='process.failed'||e.kind==='process.timeout'||e.kind==='process.silence-timeout'){c.box.classList.remove('active');c.box.classList.add('error');c.state.textContent=e.kind==='process.silence-timeout'?'yanıt vermedi · durduruldu':'hata';setTeamState(e.agent,'failed',e.kind==='process.silence-timeout'?'otomatik durduruldu':'hata',role)}else if(e.kind==='process.started'){missionCalls++;updateMissionStats();c.state.textContent='başlatıldı';setTeamState(e.agent,'working',role==='operator'?'ekibi yönetiyor':'görevi yürütüyor',role)}}
469
483
 
470
484
  /* ---- Birlesik akis: mesajlar + sistem olaylari ---- */
471
485
  function feedReady(){const feed=$('#feed');if(feed.querySelector('.empty'))feed.innerHTML='';return feed}
@@ -492,7 +506,7 @@ function renderChat(){if(!chatParent)return;$('#chatTitle').textContent=`${chatP
492
506
  async function sendChat(){const question=$('#chatQuestion').value.trim();if(!question||!chatParent)return;try{const history=$('#chatHistory');if(history.querySelector('.empty'))history.innerHTML='';history.insertAdjacentHTML('beforeend',`<div class="bubble user">${esc(question)}</div><div class="bubble operator" data-pending="true">Operatör görev kayıtlarını inceliyor…</div>`);history.scrollTop=history.scrollHeight;$('#chatState').textContent='Sohbet motoru başlatılıyor…';await api(`/api/tasks/${chatParent.id}/chat`,'POST',{question});$('#chatQuestion').value='';$('#chatState').textContent='Soru operatöre iletildi.'}catch(e){$('#chatState').textContent=e.message;const pending=$('#chatHistory [data-pending]');if(pending)pending.textContent='Soru gönderilemedi: '+e.message}}
493
507
 
494
508
  /* ---- SSE ---- */
495
- async function resultEvent(r){if(r.kind==='operator-chat'){if(chatParent&&chatParent.id===r.parentTaskId){if(r.status==='failed'){const pending=$('#chatHistory [data-pending]');if(pending){pending.removeAttribute('data-pending');pending.textContent='Operatör yanıt veremedi: '+(r.error||'Bilinmeyen hata')}$('#chatState').textContent='Sohbet başarısız.'}else{const found=await api(`/api/tasks/${r.parentTaskId}`);chatParent=found.task;renderChat();$('#chatState').textContent='Yanıt hazır.'}}return}const fileCount=(r.changes?.created||[]).length+(r.changes?.modified||[]).length+(r.changes?.deleted||[]).length,warnings=(r.delivery?.warnings||[]).length;$('#missionTitle').textContent='Görev tamamlandı';$('#missionSub').textContent=`${r.operator||'Operatör'} · ${r.rounds||0} takım turu · ${fileCount} dosya değişikliği`;teamNodes.forEach(n=>{n.node.classList.remove('working');n.node.classList.add('done');n.state.textContent='tamamlandı'});missionCompleted(`${r.operator||'Operatör'} · ${r.rounds||0} tur · ${fileCount} dosya değişikliği${r.delivery?.verification?` · ✓ ${r.delivery.verification}`:''}${warnings?` · ${warnings} uyarı`:''}`,r.summary);showToast('ok','Görev tamamlandı',r.summary||r.prompt||'Teslimat hazır.');logEvent({...r,level:'ok',msg:'Operatör görevi tamamladı'})}
509
+ async function resultEvent(r){if(r.kind==='operator-chat'){if(chatParent&&chatParent.id===r.parentTaskId){if(r.status==='failed'){const pending=$('#chatHistory [data-pending]');if(pending){pending.removeAttribute('data-pending');pending.textContent='Operatör yanıt veremedi: '+(r.error||'Bilinmeyen hata')}$('#chatState').textContent='Sohbet başarısız.'}else{const found=await api(`/api/tasks/${r.parentTaskId}`);chatParent=found.task;renderChat();$('#chatState').textContent='Yanıt hazır.'}}return}const fileCount=(r.changes?.created||[]).length+(r.changes?.modified||[]).length+(r.changes?.deleted||[]).length,warnings=(r.delivery?.warnings||[]).length;$('#missionTitle').textContent='Görev tamamlandı';$('#missionSub').textContent=`${r.operator||'Operatör'} · ${r.rounds||0} takım turu · ${fileCount} dosya değişikliği${r.usage?` · ${usageLine(r.usage)}`:''}`;teamNodes.forEach(n=>{n.node.classList.remove('working');n.node.classList.add('done');n.state.textContent='tamamlandı'});missionCompleted(`${r.operator||'Operatör'} · ${r.rounds||0} tur · ${fileCount} dosya değişikliği${r.delivery?.verification?` · ✓ ${r.delivery.verification}`:''}${warnings?` · ${warnings} uyarı`:''}`,r.summary);showToast('ok','Görev tamamlandı',r.summary||r.prompt||'Teslimat hazır.');logEvent({...r,level:'ok',msg:'Operatör görevi tamamladı'})}
496
510
  function dispatchDashboardEvent(type,data){if(type==='status')applyStatus(data);else if(type==='queue')renderQueue(data);else if(type==='activity')activity(data);else if(type==='message')message(data);else if(type==='log')logEvent(data);else if(type==='result')resultEvent(data);else if(type==='notify')desktopNotify(data);else if(type==='schedules'){STATE.schedules=data;renderScheduleList()}}
497
511
  // Görev bitince/başarısız olunca işletim sistemi bildirimi göster (panel arka planda olsa da görünür).
498
512
  // İzin bir kullanıcı hareketinde istenir (ensureNotifyPermission). Webhook'tan bağımsızdır ve yapılandırma gerektirmez.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omerrgocmen/crewctl",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "CrewCtl — Zero-dependency Node.js orchestrator that runs installed CLI coding agents as one operator-led team.",
5
5
  "keywords": [
6
6
  "ai",
@@ -39,7 +39,7 @@
39
39
  "start": "node orchestrator/src/server.js",
40
40
  "cli": "node orchestrator/src/cli.js",
41
41
  "doctor": "node orchestrator/src/doctor.js",
42
- "test": "node orchestrator/test/ui-smoke.test.js && node orchestrator/test/cli.test.js && node orchestrator/test/skills.test.js && node orchestrator/test/schedule.test.js && node orchestrator/test/team-flow.test.js && node orchestrator/test/live-diff.test.js && node orchestrator/test/checkpoints.test.js",
42
+ "test": "node orchestrator/test/ui-smoke.test.js && node orchestrator/test/cli.test.js && node orchestrator/test/server-flow.test.js && node orchestrator/test/skills.test.js && node orchestrator/test/schedule.test.js && node orchestrator/test/team-flow.test.js && node orchestrator/test/live-diff.test.js && node orchestrator/test/checkpoints.test.js",
43
43
  "prepublishOnly": "npm test"
44
44
  },
45
45
  "engines": {