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