@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,478 +1,583 @@
|
|
|
1
|
-
// store.js — dosya tabanli kuyruk, config, hafiza, butce (sifir bagimlilik)
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const crypto = require("crypto");
|
|
5
|
-
const os = require("os");
|
|
6
|
-
|
|
7
|
-
const isWin = process.platform === "win32";
|
|
8
|
-
// ASSETS: paket dizini (salt-okunur varlıklar: web/, config.default.json, varsayilan roles/+skills/).
|
|
9
|
-
// __dirname/.. daima paketin gercek yeridir (npm global/npx onbellegi dahil).
|
|
10
|
-
const ASSETS = path.join(__dirname, "..");
|
|
11
|
-
// ROOT: yazilabilir VERI koku (config.json, queue/, state/, memory/, kullanicinin roles/+skills/).
|
|
12
|
-
// Oncelik: CLI_TEAM_ROOT (izole test/CI) > CREWCTL_HOME (kullanici override) > mod tespiti.
|
|
13
|
-
// Gelistirme kopyasinda test/ klasoru bulunur ve paket dizini yazilabilirdir; o zaman ROOT=ASSETS
|
|
14
|
-
// (mevcut npm start/npm test davranisi aynen korunur). Yayinlanan pakette test/ gonderilmez, bu
|
|
15
|
-
// yuzden kurulu modda veri ~/.crewctl'e yazilir (npm onbelleğine degil).
|
|
16
|
-
const isDevCheckout = fs.existsSync(path.join(ASSETS, "test"));
|
|
17
|
-
const ROOT = path.resolve(
|
|
18
|
-
process.env.CLI_TEAM_ROOT ||
|
|
19
|
-
process.env.CREWCTL_HOME ||
|
|
20
|
-
(isDevCheckout ? ASSETS : path.join(os.homedir(), ".crewctl"))
|
|
21
|
-
);
|
|
22
|
-
// WORK_BASE: agent'larin uzerinde calisacagi klasorun goreli cozumleme tabani. Kurulu modda
|
|
23
|
-
// kullanici komutu nerede calistirdiysa orasidir (cwd); mutlak yol secilirse zaten o kullanilir.
|
|
24
|
-
const WORK_BASE = process.cwd();
|
|
25
|
-
const Q = path.join(ROOT, "queue");
|
|
26
|
-
const STATES = ["pending", "done", "failed", "approval"];
|
|
27
|
-
const MEM = path.join(ROOT, "memory");
|
|
28
|
-
const STATE = path.join(ROOT, "state");
|
|
29
|
-
const EVENTS = path.join(STATE, "events");
|
|
30
|
-
const ROLES = path.join(ROOT, "roles");
|
|
31
|
-
const SKILLS = path.join(ROOT, "skills");
|
|
32
|
-
|
|
33
|
-
// Windows'ta rename/unlink; antivirus, arama dizinleyici veya kuyrugu ayni anda tarayan
|
|
34
|
-
// canli sunucu dosyada kisa sureli bir handle tuttugunda gecici olarak EPERM/EACCES/EBUSY
|
|
35
|
-
// verir. Tek seferlik cagri bu durumda tum gorevi coldururdu; kisa artan beklemelerle yeniden
|
|
36
|
-
// deneriz. Bu kod yolu tum saveTask/saveConfig/addTask cagrilarinin sicak yolu.
|
|
37
|
-
const TRANSIENT_FS_CODES = new Set(["EPERM", "EACCES", "EBUSY", "ENOENT", "EEXIST"]);
|
|
38
|
-
function sleepSync(ms) {
|
|
39
|
-
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch {}
|
|
40
|
-
}
|
|
41
|
-
function isTransientFsError(error) {
|
|
42
|
-
return isWin && error && TRANSIENT_FS_CODES.has(error.code);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function sweepStaleTemp(dir, maxAgeMs = 5 * 60 * 1000) {
|
|
46
|
-
// Kalici olarak yeniden adlandirilamamis eski .tmp dosyalarini birak birikmesin diye temizle.
|
|
47
|
-
let entries;
|
|
48
|
-
try { entries = fs.readdirSync(dir); } catch { return; }
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
for (const name of entries) {
|
|
51
|
-
if (!name.endsWith(".tmp")) continue;
|
|
52
|
-
const full = path.join(dir, name);
|
|
53
|
-
try {
|
|
54
|
-
if (now - fs.statSync(full).mtimeMs > maxAgeMs) fs.rmSync(full, { force: true });
|
|
55
|
-
} catch {}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Kurulu modda (ROOT != ASSETS) veri kokundeki roles/ ve skills/ bos ise paketle gelen
|
|
60
|
-
// varsayilanlari BIR KEZ kopyala. Kullanicinin duzenlemeleri korunur (yalnizca bos dizine seed).
|
|
61
|
-
let seededAssets = false;
|
|
62
|
-
function seedDefaults() {
|
|
63
|
-
if (seededAssets) return;
|
|
64
|
-
seededAssets = true;
|
|
65
|
-
if (ROOT === ASSETS) return;
|
|
66
|
-
for (const [srcDir, dstDir] of [[path.join(ASSETS, "roles"), ROLES], [path.join(ASSETS, "skills"), SKILLS]]) {
|
|
67
|
-
try {
|
|
68
|
-
const existing = fs.existsSync(dstDir) ? fs.readdirSync(dstDir).filter((f) => f.toLowerCase().endsWith(".md")) : [];
|
|
69
|
-
if (existing.length) continue;
|
|
70
|
-
const defaults = fs.existsSync(srcDir) ? fs.readdirSync(srcDir).filter((f) => f.toLowerCase().endsWith(".md")) : [];
|
|
71
|
-
for (const f of defaults) fs.copyFileSync(path.join(srcDir, f), path.join(dstDir, f));
|
|
72
|
-
} catch {}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ensureDirs, olay akisinda cok sik cagrilir; temizligi dakikada bir defayla sinirla ki
|
|
77
|
-
// her stdout parcasinda dizin taramasi yapmayalim.
|
|
78
|
-
let lastSweepAt = 0;
|
|
79
|
-
function ensureDirs() {
|
|
80
|
-
const dirs = [...STATES.map((s) => path.join(Q, s)), MEM, STATE, EVENTS, ROLES, SKILLS];
|
|
81
|
-
dirs.forEach((d) => fs.mkdirSync(d, { recursive: true }));
|
|
82
|
-
seedDefaults();
|
|
83
|
-
if (Date.now() - lastSweepAt > 60 * 1000) {
|
|
84
|
-
lastSweepAt = Date.now();
|
|
85
|
-
for (const s of STATES) sweepStaleTemp(path.join(Q, s));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Atomik yazma: once .tmp'ye yaz, sonra rename et. Rename ayni disk uzerinde atomiktir;
|
|
90
|
-
// boylece surec ortasinda cokme olsa bile yarim/bozuk JSON dosyasi kalmaz (her PC'de guvenli).
|
|
91
|
-
// Windows'ta rename gecici olarak kilitlenebildigi icin (bkz. TRANSIENT_FS_CODES) yeniden dener.
|
|
92
|
-
function atomicWrite(file, data) {
|
|
93
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
94
|
-
fs.writeFileSync(tmp, data);
|
|
95
|
-
let lastError;
|
|
96
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
97
|
-
try {
|
|
98
|
-
fs.renameSync(tmp, file);
|
|
99
|
-
return;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
lastError = error;
|
|
102
|
-
if (!isTransientFsError(error)) break;
|
|
103
|
-
sleepSync(20 * (attempt + 1));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Rename kalici olarak basarisiz. Gorevi tamamen kaybetmektense hedefe dogrudan (atomik
|
|
107
|
-
// olmayan) yazmayi son care olarak dene; sonra gecici dosyayi temizle.
|
|
108
|
-
try {
|
|
109
|
-
fs.writeFileSync(file, data);
|
|
110
|
-
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
111
|
-
return;
|
|
112
|
-
} catch {}
|
|
113
|
-
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
114
|
-
throw lastError;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---- Config ----
|
|
118
|
-
// Minimal geri-donus varsayilani (config.default.json de yoksa kullanilir) — sistem her
|
|
119
|
-
// zaman calisir durumda acilsin diye.
|
|
120
|
-
const FALLBACK_CONFIG = {
|
|
121
|
-
approvalMode: "auto", workingDir: "..", dailyCallBudget: 150,
|
|
122
|
-
autonomousConsentAcceptedAt: null,
|
|
123
|
-
pollSeconds: 15, memoryCharBudget: 8000, teamContextCharBudget: 30000, agentTimeoutSeconds: 900,
|
|
124
|
-
cliSilenceTimeoutSeconds: 300,
|
|
125
|
-
discoveryIgnoredAdapters: [],
|
|
126
|
-
liveDiff: true, liveDiffIntervalMs: 2500,
|
|
127
|
-
versioning: true, versioningRetention: 20,
|
|
128
|
-
// Ajan hapsi: "workspace" = her CLI'nin native sandbox'i ile calisma klasoru DISINA yazma/ag
|
|
129
|
-
// engellenir (Docker/Git gerektirmez; codex mac/Linux/Windows'ta OS-native zorlar). "off" =
|
|
130
|
-
// eski davranis. extraWritableDirs: calisma klasoru disinda ek yazilabilir mutlak yollar.
|
|
131
|
-
sandbox: { mode: "workspace", extraWritableDirs: [] },
|
|
132
|
-
// Proje context: her calisma klasorunun .crewctl/CONTEXT.md profili gorev acilisinda yuklenir,
|
|
133
|
-
// bitiminde operator revize eder. false = eski global hafiza davranisi.
|
|
134
|
-
projectContext: true, projectContextCharBudget: 6000,
|
|
135
|
-
notify: { webhookUrl: "", onComplete: true, onFailed: true },
|
|
136
|
-
operator: { roleFile: "roles/operator.md", maxRounds: 6, maxDelegationsPerRound: 8, maxInfrastructureRecoveryRounds: 2, protocolRetries: 1 },
|
|
137
|
-
resilience: { transientRetries: 2, retryBaseSeconds: 3, maxFailoverAgents: 1 },
|
|
138
|
-
cliSettings: {
|
|
139
|
-
codex: { model: "", reasoningEffort: "medium", serviceTier: "fast" },
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return
|
|
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
|
-
task
|
|
287
|
-
task
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
function
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
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
|
-
return
|
|
409
|
-
}
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1
|
+
// store.js — dosya tabanli kuyruk, config, hafiza, butce (sifir bagimlilik)
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
const isWin = process.platform === "win32";
|
|
8
|
+
// ASSETS: paket dizini (salt-okunur varlıklar: web/, config.default.json, varsayilan roles/+skills/).
|
|
9
|
+
// __dirname/.. daima paketin gercek yeridir (npm global/npx onbellegi dahil).
|
|
10
|
+
const ASSETS = path.join(__dirname, "..");
|
|
11
|
+
// ROOT: yazilabilir VERI koku (config.json, queue/, state/, memory/, kullanicinin roles/+skills/).
|
|
12
|
+
// Oncelik: CLI_TEAM_ROOT (izole test/CI) > CREWCTL_HOME (kullanici override) > mod tespiti.
|
|
13
|
+
// Gelistirme kopyasinda test/ klasoru bulunur ve paket dizini yazilabilirdir; o zaman ROOT=ASSETS
|
|
14
|
+
// (mevcut npm start/npm test davranisi aynen korunur). Yayinlanan pakette test/ gonderilmez, bu
|
|
15
|
+
// yuzden kurulu modda veri ~/.crewctl'e yazilir (npm onbelleğine degil).
|
|
16
|
+
const isDevCheckout = fs.existsSync(path.join(ASSETS, "test"));
|
|
17
|
+
const ROOT = path.resolve(
|
|
18
|
+
process.env.CLI_TEAM_ROOT ||
|
|
19
|
+
process.env.CREWCTL_HOME ||
|
|
20
|
+
(isDevCheckout ? ASSETS : path.join(os.homedir(), ".crewctl"))
|
|
21
|
+
);
|
|
22
|
+
// WORK_BASE: agent'larin uzerinde calisacagi klasorun goreli cozumleme tabani. Kurulu modda
|
|
23
|
+
// kullanici komutu nerede calistirdiysa orasidir (cwd); mutlak yol secilirse zaten o kullanilir.
|
|
24
|
+
const WORK_BASE = process.cwd();
|
|
25
|
+
const Q = path.join(ROOT, "queue");
|
|
26
|
+
const STATES = ["pending", "done", "failed", "approval"];
|
|
27
|
+
const MEM = path.join(ROOT, "memory");
|
|
28
|
+
const STATE = path.join(ROOT, "state");
|
|
29
|
+
const EVENTS = path.join(STATE, "events");
|
|
30
|
+
const ROLES = path.join(ROOT, "roles");
|
|
31
|
+
const SKILLS = path.join(ROOT, "skills");
|
|
32
|
+
|
|
33
|
+
// Windows'ta rename/unlink; antivirus, arama dizinleyici veya kuyrugu ayni anda tarayan
|
|
34
|
+
// canli sunucu dosyada kisa sureli bir handle tuttugunda gecici olarak EPERM/EACCES/EBUSY
|
|
35
|
+
// verir. Tek seferlik cagri bu durumda tum gorevi coldururdu; kisa artan beklemelerle yeniden
|
|
36
|
+
// deneriz. Bu kod yolu tum saveTask/saveConfig/addTask cagrilarinin sicak yolu.
|
|
37
|
+
const TRANSIENT_FS_CODES = new Set(["EPERM", "EACCES", "EBUSY", "ENOENT", "EEXIST"]);
|
|
38
|
+
function sleepSync(ms) {
|
|
39
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch {}
|
|
40
|
+
}
|
|
41
|
+
function isTransientFsError(error) {
|
|
42
|
+
return isWin && error && TRANSIENT_FS_CODES.has(error.code);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sweepStaleTemp(dir, maxAgeMs = 5 * 60 * 1000) {
|
|
46
|
+
// Kalici olarak yeniden adlandirilamamis eski .tmp dosyalarini birak birikmesin diye temizle.
|
|
47
|
+
let entries;
|
|
48
|
+
try { entries = fs.readdirSync(dir); } catch { return; }
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const name of entries) {
|
|
51
|
+
if (!name.endsWith(".tmp")) continue;
|
|
52
|
+
const full = path.join(dir, name);
|
|
53
|
+
try {
|
|
54
|
+
if (now - fs.statSync(full).mtimeMs > maxAgeMs) fs.rmSync(full, { force: true });
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Kurulu modda (ROOT != ASSETS) veri kokundeki roles/ ve skills/ bos ise paketle gelen
|
|
60
|
+
// varsayilanlari BIR KEZ kopyala. Kullanicinin duzenlemeleri korunur (yalnizca bos dizine seed).
|
|
61
|
+
let seededAssets = false;
|
|
62
|
+
function seedDefaults() {
|
|
63
|
+
if (seededAssets) return;
|
|
64
|
+
seededAssets = true;
|
|
65
|
+
if (ROOT === ASSETS) return;
|
|
66
|
+
for (const [srcDir, dstDir] of [[path.join(ASSETS, "roles"), ROLES], [path.join(ASSETS, "skills"), SKILLS]]) {
|
|
67
|
+
try {
|
|
68
|
+
const existing = fs.existsSync(dstDir) ? fs.readdirSync(dstDir).filter((f) => f.toLowerCase().endsWith(".md")) : [];
|
|
69
|
+
if (existing.length) continue;
|
|
70
|
+
const defaults = fs.existsSync(srcDir) ? fs.readdirSync(srcDir).filter((f) => f.toLowerCase().endsWith(".md")) : [];
|
|
71
|
+
for (const f of defaults) fs.copyFileSync(path.join(srcDir, f), path.join(dstDir, f));
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ensureDirs, olay akisinda cok sik cagrilir; temizligi dakikada bir defayla sinirla ki
|
|
77
|
+
// her stdout parcasinda dizin taramasi yapmayalim.
|
|
78
|
+
let lastSweepAt = 0;
|
|
79
|
+
function ensureDirs() {
|
|
80
|
+
const dirs = [...STATES.map((s) => path.join(Q, s)), MEM, STATE, EVENTS, ROLES, SKILLS];
|
|
81
|
+
dirs.forEach((d) => fs.mkdirSync(d, { recursive: true }));
|
|
82
|
+
seedDefaults();
|
|
83
|
+
if (Date.now() - lastSweepAt > 60 * 1000) {
|
|
84
|
+
lastSweepAt = Date.now();
|
|
85
|
+
for (const s of STATES) sweepStaleTemp(path.join(Q, s));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Atomik yazma: once .tmp'ye yaz, sonra rename et. Rename ayni disk uzerinde atomiktir;
|
|
90
|
+
// boylece surec ortasinda cokme olsa bile yarim/bozuk JSON dosyasi kalmaz (her PC'de guvenli).
|
|
91
|
+
// Windows'ta rename gecici olarak kilitlenebildigi icin (bkz. TRANSIENT_FS_CODES) yeniden dener.
|
|
92
|
+
function atomicWrite(file, data) {
|
|
93
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
94
|
+
fs.writeFileSync(tmp, data);
|
|
95
|
+
let lastError;
|
|
96
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
97
|
+
try {
|
|
98
|
+
fs.renameSync(tmp, file);
|
|
99
|
+
return;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
lastError = error;
|
|
102
|
+
if (!isTransientFsError(error)) break;
|
|
103
|
+
sleepSync(20 * (attempt + 1));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Rename kalici olarak basarisiz. Gorevi tamamen kaybetmektense hedefe dogrudan (atomik
|
|
107
|
+
// olmayan) yazmayi son care olarak dene; sonra gecici dosyayi temizle.
|
|
108
|
+
try {
|
|
109
|
+
fs.writeFileSync(file, data);
|
|
110
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
111
|
+
return;
|
|
112
|
+
} catch {}
|
|
113
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- Config ----
|
|
118
|
+
// Minimal geri-donus varsayilani (config.default.json de yoksa kullanilir) — sistem her
|
|
119
|
+
// zaman calisir durumda acilsin diye.
|
|
120
|
+
const FALLBACK_CONFIG = {
|
|
121
|
+
approvalMode: "auto", workingDir: "..", dailyCallBudget: 150,
|
|
122
|
+
autonomousConsentAcceptedAt: null,
|
|
123
|
+
pollSeconds: 15, memoryCharBudget: 8000, teamContextCharBudget: 30000, agentTimeoutSeconds: 900,
|
|
124
|
+
cliSilenceTimeoutSeconds: 300,
|
|
125
|
+
discoveryIgnoredAdapters: [],
|
|
126
|
+
liveDiff: true, liveDiffIntervalMs: 2500,
|
|
127
|
+
versioning: true, versioningRetention: 20,
|
|
128
|
+
// Ajan hapsi: "workspace" = her CLI'nin native sandbox'i ile calisma klasoru DISINA yazma/ag
|
|
129
|
+
// engellenir (Docker/Git gerektirmez; codex mac/Linux/Windows'ta OS-native zorlar). "off" =
|
|
130
|
+
// eski davranis. extraWritableDirs: calisma klasoru disinda ek yazilabilir mutlak yollar.
|
|
131
|
+
sandbox: { mode: "workspace", extraWritableDirs: [] },
|
|
132
|
+
// Proje context: her calisma klasorunun .crewctl/CONTEXT.md profili gorev acilisinda yuklenir,
|
|
133
|
+
// bitiminde operator revize eder. false = eski global hafiza davranisi.
|
|
134
|
+
projectContext: true, projectContextCharBudget: 6000,
|
|
135
|
+
notify: { webhookUrl: "", onComplete: true, onFailed: true },
|
|
136
|
+
operator: { roleFile: "roles/operator.md", maxRounds: 6, maxDelegationsPerRound: 8, maxInfrastructureRecoveryRounds: 2, protocolRetries: 1 },
|
|
137
|
+
resilience: { transientRetries: 2, retryBaseSeconds: 3, maxFailoverAgents: 1 },
|
|
138
|
+
cliSettings: {
|
|
139
|
+
codex: { model: "", reasoningEffort: "medium", serviceTier: "fast" },
|
|
140
|
+
claude: { model: "" },
|
|
141
|
+
opencode: { model: "", modelPreferences: ["*deepseek*free*", "*free*", "*"], modelExclude: ["ollama/*", "lmstudio/*", "llamacpp/*", "local/*", "localhost/*"] },
|
|
142
|
+
},
|
|
143
|
+
skills: { enabled: [], autoMatch: true, catalogLimit: 12, maxSkillsPerAssignment: 3, charBudget: 2400, referenceCharBudget: 1200 },
|
|
144
|
+
agents: {}, riskyPatterns: [], schedules: [],
|
|
145
|
+
};
|
|
146
|
+
// Eski sema, model ayarlarini operator altina gomuyordu (operator.codexSettings ve
|
|
147
|
+
// operator.model). Yeni sema CLI bazlidir: cliSettings[adapter] o CLI'nin operator ve
|
|
148
|
+
// uzman kullanimlarinin tumune uygulanir. Saf ve idempotent olmali; loadConfig sicak yoldur.
|
|
149
|
+
function normalizeConfig(cfg) {
|
|
150
|
+
if (!cfg || typeof cfg !== "object") return cfg;
|
|
151
|
+
const current = cfg.cliSettings && typeof cfg.cliSettings === "object" ? cfg.cliSettings : {};
|
|
152
|
+
const legacyCodex = cfg.operator && typeof cfg.operator.codexSettings === "object" ? cfg.operator.codexSettings : {};
|
|
153
|
+
const legacyOpenCodeModel = typeof cfg.operator?.model === "string" ? cfg.operator.model : "";
|
|
154
|
+
const normalized = {
|
|
155
|
+
...cfg,
|
|
156
|
+
skills: { enabled: [], autoMatch: true, catalogLimit: 12, maxSkillsPerAssignment: 3, charBudget: 2400, referenceCharBudget: 1200, ...(cfg.skills || {}) },
|
|
157
|
+
schedules: Array.isArray(cfg.schedules) ? cfg.schedules : [],
|
|
158
|
+
// Proje = kaydedilmis, isimli calisma klasoru. Gorevler projectId ile bir projeye baglanabilir;
|
|
159
|
+
// targetDir geriye-uyum icin kalir (projesiz/eski gorevler aynen calisir).
|
|
160
|
+
projects: Array.isArray(cfg.projects) ? cfg.projects : [],
|
|
161
|
+
// Her yuklemede hapis semasini garanti et; boylece eski config.json'lar yukseltilince
|
|
162
|
+
// varsayilan olarak "workspace" hapsini kazanir. Gecersiz mode "workspace"e duser.
|
|
163
|
+
sandbox: {
|
|
164
|
+
mode: ["off", "workspace"].includes(cfg.sandbox?.mode) ? cfg.sandbox.mode : "workspace",
|
|
165
|
+
extraWritableDirs: Array.isArray(cfg.sandbox?.extraWritableDirs)
|
|
166
|
+
? cfg.sandbox.extraWritableDirs.filter((x) => typeof x === "string" && x.trim()).map(String)
|
|
167
|
+
: [],
|
|
168
|
+
},
|
|
169
|
+
cliSettings: {
|
|
170
|
+
codex: { model: "", reasoningEffort: "medium", serviceTier: "fast", ...legacyCodex, ...(current.codex || {}) },
|
|
171
|
+
claude: { model: "", ...(current.claude || {}) },
|
|
172
|
+
// Desenler mevcut config'lere de eklenir ki kullanici bunlari Ayarlar'da gorup
|
|
173
|
+
// kendi aboneligine gore duzenleyebilsin. Kendi degerini yazan kullanicininki korunur.
|
|
174
|
+
opencode: {
|
|
175
|
+
model: legacyOpenCodeModel,
|
|
176
|
+
modelPreferences: ["*deepseek*free*", "*free*", "*"],
|
|
177
|
+
modelExclude: ["ollama/*", "lmstudio/*", "llamacpp/*", "local/*", "localhost/*"],
|
|
178
|
+
...(current.opencode || {}),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
if (cfg.operator && typeof cfg.operator === "object") {
|
|
183
|
+
normalized.operator = { ...cfg.operator };
|
|
184
|
+
delete normalized.operator.codexSettings;
|
|
185
|
+
delete normalized.operator.model;
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
// config.json kisiye ozeldir (gitignore). Yoksa config.default.json sablonundan uretilir;
|
|
190
|
+
// boylece kullanici klonlayip `npm start` dedigi anda calisir ve kurulu CLI'lar keşifle eklenir.
|
|
191
|
+
// Paketle gelen beceri dosyalarinin slug'lari (dosya adi = frontmatter name, dogrulanir).
|
|
192
|
+
// Ilk config uretiminde beceriler VARSAYILAN OLARAK ETKIN gelsin diye kullanilir; boylece
|
|
193
|
+
// operator, kullanicinin becerilerini kodlama/planlama/review delegasyonlarinda kullanabilir.
|
|
194
|
+
function shippedSkillSlugs() {
|
|
195
|
+
try {
|
|
196
|
+
return fs.readdirSync(path.join(ASSETS, "skills"))
|
|
197
|
+
.filter((f) => f.toLowerCase().endsWith(".md"))
|
|
198
|
+
.map((f) => f.replace(/\.md$/i, ""));
|
|
199
|
+
} catch { return []; }
|
|
200
|
+
}
|
|
201
|
+
function loadConfig() {
|
|
202
|
+
const file = path.join(ROOT, "config.json");
|
|
203
|
+
if (!fs.existsSync(file)) {
|
|
204
|
+
const template = path.join(ASSETS, "config.default.json");
|
|
205
|
+
const seed = fs.existsSync(template) ? fs.readFileSync(template, "utf8") : JSON.stringify(FALLBACK_CONFIG, null, 2);
|
|
206
|
+
let seedObj;
|
|
207
|
+
try { seedObj = JSON.parse(seed); } catch { seedObj = { ...FALLBACK_CONFIG }; }
|
|
208
|
+
// Ilk kurulumda tum paket becerilerini etkinlestir (kullanici Ayarlar'dan kapatabilir).
|
|
209
|
+
// Sonraki yuklemelerde dosya var oldugundan bu blok CALISMAZ; kullanicinin secimi korunur.
|
|
210
|
+
seedObj.skills = seedObj.skills || {};
|
|
211
|
+
if (!Array.isArray(seedObj.skills.enabled) || !seedObj.skills.enabled.length) {
|
|
212
|
+
const slugs = shippedSkillSlugs();
|
|
213
|
+
if (slugs.length) seedObj.skills.enabled = slugs;
|
|
214
|
+
}
|
|
215
|
+
atomicWrite(file, JSON.stringify(seedObj, null, 2));
|
|
216
|
+
}
|
|
217
|
+
return normalizeConfig(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
218
|
+
}
|
|
219
|
+
function saveConfig(cfg) {
|
|
220
|
+
atomicWrite(path.join(ROOT, "config.json"), JSON.stringify(cfg, null, 2));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- Roller ----
|
|
224
|
+
function listRoles() {
|
|
225
|
+
if (!fs.existsSync(ROLES)) return [];
|
|
226
|
+
return fs.readdirSync(ROLES).filter((f) => f.endsWith(".md"));
|
|
227
|
+
}
|
|
228
|
+
function readRole(file) {
|
|
229
|
+
const p = path.join(ROLES, path.basename(file));
|
|
230
|
+
return fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
231
|
+
}
|
|
232
|
+
function writeRole(file, content) {
|
|
233
|
+
fs.writeFileSync(path.join(ROLES, path.basename(file)), content);
|
|
234
|
+
}
|
|
235
|
+
function deleteRole(file) {
|
|
236
|
+
const p = path.join(ROLES, path.basename(file));
|
|
237
|
+
if (fs.existsSync(p)) fs.rmSync(p);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---- Gorevler ----
|
|
241
|
+
function taskPath(state, id) {
|
|
242
|
+
return path.join(Q, state, `${id}.json`);
|
|
243
|
+
}
|
|
244
|
+
function listTasks(state) {
|
|
245
|
+
const dir = path.join(Q, state);
|
|
246
|
+
if (!fs.existsSync(dir)) return [];
|
|
247
|
+
return fs
|
|
248
|
+
.readdirSync(dir)
|
|
249
|
+
.filter((f) => f.endsWith(".json"))
|
|
250
|
+
.map((f) => {
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
259
|
+
}
|
|
260
|
+
// Yalnizca en yeni `limit` gorevi okur. Gorev id'si zaman damgasiyla basladigi icin dosya adi
|
|
261
|
+
// siralamasi = kronolojik siralama; boylece done/failed gecmisi buyudukce (yuzlerce/binlerce
|
|
262
|
+
// dosya) her pano yenilemesinde TUM dosyalari okuyup ayristirma maliyeti sabit N'e iner.
|
|
263
|
+
function listRecentTasks(state, limit) {
|
|
264
|
+
const dir = path.join(Q, state);
|
|
265
|
+
if (!fs.existsSync(dir)) return [];
|
|
266
|
+
let names = fs.readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
|
|
267
|
+
if (limit && names.length > limit) names = names.slice(-limit);
|
|
268
|
+
return names
|
|
269
|
+
.map((f) => {
|
|
270
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; }
|
|
271
|
+
})
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
}
|
|
274
|
+
function nextPending() {
|
|
275
|
+
return listTasks("pending")[0] || null;
|
|
276
|
+
}
|
|
277
|
+
function addTask(prompt, targetDir, operatorCli, executionMode) {
|
|
278
|
+
const id =
|
|
279
|
+
new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 15) +
|
|
280
|
+
"-" +
|
|
281
|
+
Math.floor(Math.random() * 9000 + 1000);
|
|
282
|
+
const task = { id, prompt, status: "pending", createdAt: new Date().toISOString() };
|
|
283
|
+
if (targetDir) task.targetDir = targetDir;
|
|
284
|
+
if (operatorCli) task.operatorCli = operatorCli;
|
|
285
|
+
if (executionMode) task.executionMode = executionMode;
|
|
286
|
+
atomicWrite(taskPath("pending", id), JSON.stringify(task, null, 2));
|
|
287
|
+
return task;
|
|
288
|
+
}
|
|
289
|
+
// Yuklenen ekli dosyalarin staging kokudur: ROOT/state/attachments/<taskId>/. Icerik KUYRUK
|
|
290
|
+
// JSON'una degil diske yazilir (kuyruk dosyasi base64 ile sismesin); motor gorev calisirken
|
|
291
|
+
// bunlari calisma klasorunun .crewctl/attachments/'ina kopyalar.
|
|
292
|
+
function attachmentStagingDir(taskId) {
|
|
293
|
+
return path.join(STATE, "attachments", String(taskId));
|
|
294
|
+
}
|
|
295
|
+
// UI'dan gelen [{name, dataBase64}] eklerini staging'e yazar. Ad guvenlik icin basename'e
|
|
296
|
+
// indirgenir ve tehlikeli karakterler temizlenir. Diske yazilan [{name,size}] listesini doner.
|
|
297
|
+
function saveAttachments(taskId, files) {
|
|
298
|
+
const out = [];
|
|
299
|
+
if (!Array.isArray(files) || !files.length) return out;
|
|
300
|
+
const dir = attachmentStagingDir(taskId);
|
|
301
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
302
|
+
for (const f of files) {
|
|
303
|
+
if (!f || typeof f.name !== "string" || typeof f.dataBase64 !== "string") continue;
|
|
304
|
+
const safe = path.basename(f.name).replace(/[\\/:*?"<>|]/g, "_").slice(0, 200).trim();
|
|
305
|
+
if (!safe || safe === "." || safe === "..") continue;
|
|
306
|
+
let buf;
|
|
307
|
+
try { buf = Buffer.from(f.dataBase64, "base64"); } catch { continue; }
|
|
308
|
+
try { fs.writeFileSync(path.join(dir, safe), buf); } catch { continue; }
|
|
309
|
+
out.push({ name: safe, size: buf.length });
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
// ---- Projeler (kaydedilmis calisma klasorleri) ----
|
|
314
|
+
function newProjectId() {
|
|
315
|
+
return "proj-" + Date.now().toString(36) + "-" + Math.floor(Math.random() * 46656).toString(36).padStart(3, "0");
|
|
316
|
+
}
|
|
317
|
+
// UI'dan gelen proje verisini dogrular ve normalize eder. name + path zorunlu; path mutlak yola
|
|
318
|
+
// cozulur (gorev bu yolda calisir). operatorCli/defaultMode opsiyonel proje varsayilanlaridir.
|
|
319
|
+
function normalizeProject(input) {
|
|
320
|
+
if (!input || typeof input !== "object") throw new Error("Proje verisi gecersiz.");
|
|
321
|
+
const name = String(input.name || "").trim();
|
|
322
|
+
if (!name) throw new Error("Proje adi gerekli.");
|
|
323
|
+
const rawPath = String(input.path || "").trim();
|
|
324
|
+
if (!rawPath) throw new Error("Proje klasoru gerekli.");
|
|
325
|
+
const out = { name: name.slice(0, 120), path: path.resolve(rawPath) };
|
|
326
|
+
if (input.operatorCli && typeof input.operatorCli === "string") out.operatorCli = input.operatorCli;
|
|
327
|
+
if (["auto", "fast", "balanced", "deep"].includes(input.defaultMode)) out.defaultMode = input.defaultMode;
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function listProjects() {
|
|
331
|
+
const list = loadConfig().projects;
|
|
332
|
+
return Array.isArray(list) ? list : [];
|
|
333
|
+
}
|
|
334
|
+
function getProject(id) {
|
|
335
|
+
return listProjects().find((p) => p.id === id) || null;
|
|
336
|
+
}
|
|
337
|
+
function addProject(input) {
|
|
338
|
+
const project = normalizeProject(input);
|
|
339
|
+
project.id = newProjectId();
|
|
340
|
+
project.createdAt = new Date().toISOString();
|
|
341
|
+
project.lastUsedAt = project.createdAt;
|
|
342
|
+
const cfg = loadConfig();
|
|
343
|
+
cfg.projects = [...(cfg.projects || []), project];
|
|
344
|
+
saveConfig(cfg);
|
|
345
|
+
return project;
|
|
346
|
+
}
|
|
347
|
+
function updateProject(id, patch) {
|
|
348
|
+
const cfg = loadConfig();
|
|
349
|
+
const list = cfg.projects || [];
|
|
350
|
+
const index = list.findIndex((p) => p.id === id);
|
|
351
|
+
if (index < 0) return null;
|
|
352
|
+
const merged = normalizeProject({ ...list[index], ...patch });
|
|
353
|
+
merged.id = list[index].id;
|
|
354
|
+
merged.createdAt = list[index].createdAt;
|
|
355
|
+
merged.lastUsedAt = new Date().toISOString();
|
|
356
|
+
list[index] = merged;
|
|
357
|
+
cfg.projects = list;
|
|
358
|
+
saveConfig(cfg);
|
|
359
|
+
return merged;
|
|
360
|
+
}
|
|
361
|
+
function touchProject(id) {
|
|
362
|
+
const cfg = loadConfig();
|
|
363
|
+
const list = cfg.projects || [];
|
|
364
|
+
const project = list.find((p) => p.id === id);
|
|
365
|
+
if (!project) return null;
|
|
366
|
+
project.lastUsedAt = new Date().toISOString();
|
|
367
|
+
cfg.projects = list;
|
|
368
|
+
saveConfig(cfg);
|
|
369
|
+
return project;
|
|
370
|
+
}
|
|
371
|
+
function deleteProject(id) {
|
|
372
|
+
const cfg = loadConfig();
|
|
373
|
+
const list = cfg.projects || [];
|
|
374
|
+
if (!list.some((p) => p.id === id)) return false;
|
|
375
|
+
cfg.projects = list.filter((p) => p.id !== id);
|
|
376
|
+
saveConfig(cfg);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
function addChatTask(parentTask, question) {
|
|
380
|
+
const task = addTask(question, parentTask.targetDir, parentTask.operatorCli, "chat");
|
|
381
|
+
task.kind = "operator-chat";
|
|
382
|
+
task.parentTaskId = parentTask.id;
|
|
383
|
+
saveTask("pending", task);
|
|
384
|
+
return task;
|
|
385
|
+
}
|
|
386
|
+
// Zamanlanmis bir gorevi kuyruga ekler. Normal gorevle ayni yolu kullanir; yalnizca kaynagini
|
|
387
|
+
// isaretlemek icin scheduleId iliştirir (Pano bu görevi "zamanlanmis" rozetiyle gosterebilsin).
|
|
388
|
+
function addScheduledTask(schedule) {
|
|
389
|
+
const task = addTask(schedule.prompt, schedule.targetDir, schedule.operatorCli, schedule.executionMode || "auto");
|
|
390
|
+
task.scheduleId = schedule.id;
|
|
391
|
+
saveTask("pending", task);
|
|
392
|
+
return task;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---- Kalici calisma olaylari ----
|
|
396
|
+
function appendRunEvent(taskId, event) {
|
|
397
|
+
ensureDirs();
|
|
398
|
+
fs.appendFileSync(path.join(EVENTS, `${path.basename(taskId)}.jsonl`), JSON.stringify(event) + "\n");
|
|
399
|
+
}
|
|
400
|
+
function listRunEvents(taskId, limit = 1000) {
|
|
401
|
+
const file = path.join(EVENTS, `${path.basename(taskId)}.jsonl`);
|
|
402
|
+
if (!fs.existsSync(file)) return [];
|
|
403
|
+
return fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).slice(-Math.max(1, limit)).map((line) => {
|
|
404
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
405
|
+
}).filter(Boolean);
|
|
406
|
+
}
|
|
407
|
+
function hashText(text) {
|
|
408
|
+
return crypto.createHash("sha256").update(String(text)).digest("hex");
|
|
409
|
+
}
|
|
410
|
+
function saveTask(state, task) {
|
|
411
|
+
atomicWrite(taskPath(state, task.id), JSON.stringify(task, null, 2));
|
|
412
|
+
}
|
|
413
|
+
function findTask(id) {
|
|
414
|
+
for (const s of STATES) {
|
|
415
|
+
const p = taskPath(s, id);
|
|
416
|
+
if (fs.existsSync(p)) return { state: s, task: JSON.parse(fs.readFileSync(p, "utf8")) };
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
function moveTask(fromState, toState, task) {
|
|
421
|
+
const src = taskPath(fromState, task.id);
|
|
422
|
+
// Hedefi once atomik olarak yaz. Surec iki islem arasinda cokse bile gorev kaybolmaz.
|
|
423
|
+
saveTask(toState, task);
|
|
424
|
+
if (fromState !== toState && fs.existsSync(src)) fs.rmSync(src);
|
|
425
|
+
}
|
|
426
|
+
function removeTask(state, id) {
|
|
427
|
+
const p = taskPath(state, id);
|
|
428
|
+
if (fs.existsSync(p)) fs.rmSync(p);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function approveTask(id) {
|
|
432
|
+
const found = findTask(id);
|
|
433
|
+
if (!found) throw new Error(`Gorev bulunamadi: ${id}`);
|
|
434
|
+
if (found.state !== "approval") throw new Error(`Gorev onay beklemiyor: ${id}`);
|
|
435
|
+
const task = found.task;
|
|
436
|
+
if (task.planHash && task.planPreview && task.planHash !== hashText(task.planPreview)) {
|
|
437
|
+
throw new Error("Plan onay beklerken degismis; yeniden planlanmasi gerekiyor.");
|
|
438
|
+
}
|
|
439
|
+
task.approved = true;
|
|
440
|
+
task.status = "pending";
|
|
441
|
+
moveTask(found.state, "pending", task);
|
|
442
|
+
return task;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function rejectTask(id) {
|
|
446
|
+
const found = findTask(id);
|
|
447
|
+
if (!found) throw new Error(`Gorev bulunamadi: ${id}`);
|
|
448
|
+
if (found.state !== "approval") throw new Error(`Gorev onay beklemiyor: ${id}`);
|
|
449
|
+
found.task.status = "rejected";
|
|
450
|
+
found.task.finishedAt = new Date().toISOString();
|
|
451
|
+
moveTask(found.state, "failed", found.task);
|
|
452
|
+
return found.task;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ---- Hafiza ----
|
|
456
|
+
function getMemory(charBudget) {
|
|
457
|
+
if (!fs.existsSync(MEM)) return "(hafiza bos)";
|
|
458
|
+
const files = fs
|
|
459
|
+
.readdirSync(MEM)
|
|
460
|
+
.filter((f) => f.endsWith(".md"))
|
|
461
|
+
.map((f) => path.join(MEM, f));
|
|
462
|
+
if (!files.length) return "(hafiza bos)";
|
|
463
|
+
let text = files.map((f) => fs.readFileSync(f, "utf8")).join("\n\n");
|
|
464
|
+
if (charBudget && text.length > charBudget) text = text.slice(-charBudget);
|
|
465
|
+
return text;
|
|
466
|
+
}
|
|
467
|
+
function appendMemory(title, body) {
|
|
468
|
+
const stamp = new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
469
|
+
fs.appendFileSync(path.join(MEM, "log.md"), `\n## ${stamp} — ${title}\n${body}\n`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---- Proje context (path'e ozel) ----
|
|
473
|
+
// Global log.md'nin aksine, her calisma klasorunun KENDI birikmis proje profili vardir:
|
|
474
|
+
// <cwd>/.crewctl/CONTEXT.md. Gorev acilisinda operatore yuklenir (tum kodu bastan taramaya
|
|
475
|
+
// gerek kalmasin), gorev bitiminde operator tarafindan REVIZE edilir (append degil). Calisma
|
|
476
|
+
// klasorunde durdugu icin seffaf, elle duzenlenebilir ve git'e girebilir.
|
|
477
|
+
function projectContextPath(cwd) {
|
|
478
|
+
return path.join(cwd, ".crewctl", "CONTEXT.md");
|
|
479
|
+
}
|
|
480
|
+
function readProjectContext(cwd) {
|
|
481
|
+
if (!cwd) return "";
|
|
482
|
+
try { return fs.readFileSync(projectContextPath(cwd), "utf8"); } catch { return ""; }
|
|
483
|
+
}
|
|
484
|
+
function writeProjectContext(cwd, content) {
|
|
485
|
+
if (!cwd) return false;
|
|
486
|
+
try {
|
|
487
|
+
fs.mkdirSync(path.join(cwd, ".crewctl"), { recursive: true });
|
|
488
|
+
atomicWrite(projectContextPath(cwd), String(content == null ? "" : content));
|
|
489
|
+
return true;
|
|
490
|
+
} catch { return false; }
|
|
491
|
+
}
|
|
492
|
+
function clearProjectContext(cwd) {
|
|
493
|
+
if (!cwd) return false;
|
|
494
|
+
try { fs.rmSync(projectContextPath(cwd), { force: true }); return true; } catch { return false; }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---- Gunluk cagri butcesi ----
|
|
498
|
+
function callFile() {
|
|
499
|
+
return path.join(STATE, `calls-${new Date().toISOString().slice(0, 10)}.txt`);
|
|
500
|
+
}
|
|
501
|
+
function getCallCount() {
|
|
502
|
+
const f = callFile();
|
|
503
|
+
return fs.existsSync(f) ? parseInt(fs.readFileSync(f, "utf8"), 10) || 0 : 0;
|
|
504
|
+
}
|
|
505
|
+
function bumpCallCount() {
|
|
506
|
+
const n = getCallCount() + 1;
|
|
507
|
+
fs.writeFileSync(callFile(), String(n));
|
|
508
|
+
return n;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---- Gunluk token/maliyet birikimi ----
|
|
512
|
+
// Cagri sayacinin yanina gunluk token ve maliyet toplami. Ayri dosyada tutulur ki mevcut
|
|
513
|
+
// calls-*.txt bicimi (ve onu okuyan cli.js) hic degismesin. Bozuk/eksik dosya sifir sayilir;
|
|
514
|
+
// telemetri hicbir kosulda gorev akisini durdurmamali.
|
|
515
|
+
const USAGE_FIELDS = ["input", "output", "reasoning", "cacheRead", "cacheWrite", "cost"];
|
|
516
|
+
function usageFile(day) {
|
|
517
|
+
return path.join(STATE, `usage-${day || new Date().toISOString().slice(0, 10)}.json`);
|
|
518
|
+
}
|
|
519
|
+
function getDailyUsage(day) {
|
|
520
|
+
const empty = Object.fromEntries(USAGE_FIELDS.map((k) => [k, 0]));
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(fs.readFileSync(usageFile(day), "utf8"));
|
|
523
|
+
for (const key of USAGE_FIELDS) empty[key] = Number(parsed?.[key]) || 0;
|
|
524
|
+
empty.calls = Number(parsed?.calls) || 0;
|
|
525
|
+
} catch { empty.calls = 0; }
|
|
526
|
+
return empty;
|
|
527
|
+
}
|
|
528
|
+
function addDailyUsage(usage) {
|
|
529
|
+
const current = getDailyUsage();
|
|
530
|
+
for (const key of USAGE_FIELDS) current[key] += Number(usage?.[key]) || 0;
|
|
531
|
+
current.calls += 1;
|
|
532
|
+
atomicWrite(usageFile(), JSON.stringify(current));
|
|
533
|
+
return current;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
module.exports = {
|
|
537
|
+
ROOT,
|
|
538
|
+
ASSETS,
|
|
539
|
+
WORK_BASE,
|
|
540
|
+
ensureDirs,
|
|
541
|
+
loadConfig,
|
|
542
|
+
normalizeConfig,
|
|
543
|
+
saveConfig,
|
|
544
|
+
listRoles,
|
|
545
|
+
readRole,
|
|
546
|
+
writeRole,
|
|
547
|
+
deleteRole,
|
|
548
|
+
listTasks,
|
|
549
|
+
listRecentTasks,
|
|
550
|
+
nextPending,
|
|
551
|
+
addTask,
|
|
552
|
+
attachmentStagingDir,
|
|
553
|
+
saveAttachments,
|
|
554
|
+
newProjectId,
|
|
555
|
+
normalizeProject,
|
|
556
|
+
listProjects,
|
|
557
|
+
getProject,
|
|
558
|
+
addProject,
|
|
559
|
+
updateProject,
|
|
560
|
+
touchProject,
|
|
561
|
+
deleteProject,
|
|
562
|
+
addChatTask,
|
|
563
|
+
addScheduledTask,
|
|
564
|
+
saveTask,
|
|
565
|
+
findTask,
|
|
566
|
+
moveTask,
|
|
567
|
+
removeTask,
|
|
568
|
+
approveTask,
|
|
569
|
+
rejectTask,
|
|
570
|
+
getMemory,
|
|
571
|
+
appendMemory,
|
|
572
|
+
readProjectContext,
|
|
573
|
+
writeProjectContext,
|
|
574
|
+
clearProjectContext,
|
|
575
|
+
projectContextPath,
|
|
576
|
+
getCallCount,
|
|
577
|
+
bumpCallCount,
|
|
578
|
+
getDailyUsage,
|
|
579
|
+
addDailyUsage,
|
|
580
|
+
appendRunEvent,
|
|
581
|
+
listRunEvents,
|
|
582
|
+
hashText,
|
|
583
|
+
};
|