@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
|
-
|
|
306
|
-
|
|
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"))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 · ${
|
|
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.
|
|
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": {
|