dsh-codebase-chat 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,2857 @@
1
+ import { readFile, readdir, stat, writeFile, mkdir } from "node:fs/promises";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { basename, extname, join, resolve, isAbsolute, relative, sep, dirname, posix as posixPath } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { exec } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import figlet from "figlet";
8
+ import {
9
+ loadCache,
10
+ saveCache,
11
+ hashFile,
12
+ fileStats,
13
+ estimateTokens,
14
+ truncateByTokens
15
+ } from "./cache.js";
16
+
17
+ let defineTool;
18
+ let createUserMessage;
19
+ try {
20
+ ({ defineTool } = await import("@deepseek-ai/dsh-tools"));
21
+ } catch { defineTool = undefined; }
22
+ try {
23
+ ({ createUserMessage } = await import("@deepseek-ai/dsh-llm"));
24
+ } catch { createUserMessage = undefined; }
25
+
26
+ const name = "codebase-chat";
27
+ const inject = ["tools", "commands", "agents", "systemPrompt"];
28
+
29
+ const VERSION = "0.16.0";
30
+ const execAsync = promisify(exec);
31
+
32
+ const PROTECTED_PATHS = [
33
+ "C:\\Users\\shinzarou-eng\\Downloads\\marketing-session-app",
34
+ "marketing-session-app"
35
+ ];
36
+
37
+ const SOURCE_EXTS = new Set([
38
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
39
+ ".vue", ".svelte", ".py", ".rs", ".go", ".java", ".kt",
40
+ ".swift", ".cs", ".cpp", ".c", ".h", ".hpp",
41
+ ".css", ".scss", ".less", ".html", ".json", ".yaml", ".yml", ".md"
42
+ ]);
43
+
44
+ const SKIP_DIRS = new Set([
45
+ "node_modules", ".git", "dist", "build", "out", ".output",
46
+ "coverage", "tmp", "temp", ".cache", ".turbo", ".next",
47
+ "android", "ios", "e2e-shots", "playstore_screenshots",
48
+ ".cursor", ".idea", ".memsearch", ".vscode", "__pycache__"
49
+ ]);
50
+
51
+ const MAX_FILE_BYTES = 16_000;
52
+ const MAX_TOTAL_BYTES = 200_000;
53
+ const MAX_CONTEXT_TOKENS = 60_000;
54
+ const MAX_TREE_LINES = 500;
55
+ const MAX_SEARCH_FILES = 80;
56
+
57
+ const progressStore = new Map();
58
+ let currentProgressSession = null;
59
+ let currentProgressRoot = null;
60
+ let lastFileReadAt = 0;
61
+
62
+ function setProgressSession(sessionId) {
63
+ currentProgressSession = sessionId;
64
+ currentProgressRoot = null;
65
+ lastFileReadAt = 0;
66
+ if (sessionId) progressStore.set(sessionId, []);
67
+ }
68
+
69
+ function clearProgressSession() {
70
+ currentProgressSession = null;
71
+ currentProgressRoot = null;
72
+ }
73
+
74
+ function setProgressRoot(root) {
75
+ currentProgressRoot = root;
76
+ }
77
+
78
+ function reportProgress(message) {
79
+ if (!currentProgressSession) return;
80
+ const list = progressStore.get(currentProgressSession) || [];
81
+ list.push({ t: Date.now(), message });
82
+ if (list.length > 40) list.shift();
83
+ progressStore.set(currentProgressSession, list);
84
+ }
85
+
86
+ function reportFileRead(filePath) {
87
+ if (!currentProgressSession || !currentProgressRoot) return;
88
+ const now = Date.now();
89
+ if (now - lastFileReadAt < 120) return;
90
+ lastFileReadAt = now;
91
+ const rel = relative(currentProgressRoot, filePath).split(sep).join("/");
92
+ reportProgress(`Lecture : ${rel}`);
93
+ }
94
+
95
+ function isProtectedPath(input) {
96
+ const normalized = input.toLowerCase().replace(/\//g, "\\");
97
+ return PROTECTED_PATHS.some((p) => normalized === p.toLowerCase() || normalized.startsWith(p.toLowerCase() + "\\"));
98
+ }
99
+
100
+ async function safeReadText(filePath) {
101
+ try {
102
+ const text = Buffer.from(await readFile(filePath)).toString("utf8");
103
+ reportFileRead(filePath);
104
+ return text;
105
+ } catch {
106
+ return void 0;
107
+ }
108
+ }
109
+
110
+ async function isDirectory(path) {
111
+ try {
112
+ return (await stat(path)).isDirectory();
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function resolveAlias(projectPath) {
119
+ const lower = (projectPath ?? "").trim().toLowerCase().replace(/['"]/g, "");
120
+ if (lower === "dako") return "D:\\Nouveau dossier";
121
+ if (lower.startsWith("dako")) return "D:\\Nouveau dossier";
122
+ const env = process.env.DSH_DAKO_PROJECT;
123
+ if (env && lower === "dako") return env;
124
+ return (projectPath ?? "").trim().replace(/['"]/g, "");
125
+ }
126
+
127
+ async function detectProjectRoot(startDir = process.cwd()) {
128
+ const markers = ["package.json", "AGENTS.md", ".git", "tsconfig.json"];
129
+ const home = homedir();
130
+ let current = resolve(startDir);
131
+ const maxSteps = 20;
132
+ for (let i = 0; i < maxSteps; i++) {
133
+ if (isProtectedPath(current)) break;
134
+ for (const m of markers) {
135
+ try {
136
+ const s = await stat(join(current, m));
137
+ if (s.isFile() || s.isDirectory()) return current;
138
+ } catch { /* ignore */ }
139
+ }
140
+ const parent = dirname(current);
141
+ if (parent === current || current.toLowerCase() === home.toLowerCase()) break;
142
+ current = parent;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ async function resolveProjectPath(projectPath) {
148
+ if (!projectPath || typeof projectPath !== "string" || projectPath.trim() === "." || projectPath.trim().toLowerCase() === "current") {
149
+ reportProgress("Détection du projet dans le dossier courant...");
150
+ const detected = await detectProjectRoot();
151
+ if (detected) {
152
+ reportProgress(`Projet détecté : ${detected}`);
153
+ setProgressRoot(detected);
154
+ reportProgress(`Projet prêt : ${detected}`);
155
+ return detected;
156
+ }
157
+ throw new Error("Aucun projet detecte depuis le dossier courant. Fournis --project <chemin>.");
158
+ }
159
+ reportProgress(`Résolution du chemin : ${projectPath}`);
160
+ const aliased = resolveAlias(projectPath);
161
+ const absProject = isAbsolute(aliased) ? resolve(aliased) : resolve(process.cwd(), aliased);
162
+ if (isProtectedPath(absProject)) throw new Error("Ce chemin est protege et ne peut pas etre lu.");
163
+ if (!await isDirectory(absProject)) throw new Error(`Le chemin n'est pas un dossier : ${projectPath}`);
164
+ setProgressRoot(absProject);
165
+ reportProgress(`Projet prêt : ${absProject}`);
166
+ return absProject;
167
+ }
168
+
169
+ async function findProjectRoot(absProject) {
170
+ const srcDir = join(absProject, "src");
171
+ return (await isDirectory(srcDir)) ? srcDir : absProject;
172
+ }
173
+
174
+ async function getProjectName(absProject) {
175
+ let rawName = "";
176
+ try {
177
+ const text = await safeReadText(join(absProject, "package.json"));
178
+ if (text) {
179
+ const parsed = JSON.parse(text);
180
+ rawName = parsed.name || "";
181
+ }
182
+ } catch {
183
+ // ignore
184
+ }
185
+ const folder = basename(absProject).toLowerCase();
186
+ if (folder.includes("nouveau dossier") || rawName.toLowerCase() === "dako-app" || rawName.toLowerCase().startsWith("dako")) return "Dako";
187
+ return rawName || basename(absProject);
188
+ }
189
+
190
+ async function getPackageSummary(absProject) {
191
+ const summary = { name: "", type: "", scripts: {}, dependencies: [], devDependencies: [], main: "" };
192
+ try {
193
+ const text = await safeReadText(join(absProject, "package.json"));
194
+ if (!text) return summary;
195
+ const parsed = JSON.parse(text);
196
+ summary.name = parsed.name || "";
197
+ summary.type = parsed.type || "commonjs";
198
+ summary.scripts = parsed.scripts || {};
199
+ summary.dependencies = Object.keys(parsed.dependencies || {});
200
+ summary.devDependencies = Object.keys(parsed.devDependencies || {});
201
+ summary.main = parsed.main || "";
202
+ } catch {
203
+ // ignore
204
+ }
205
+ return summary;
206
+ }
207
+
208
+ function parseImports(text, relPath) {
209
+ const imports = [];
210
+ const seen = new Set();
211
+ const patterns = [
212
+ /import\s+(?:(?:[\s\S]*?)\s+from\s+)?['"]([^'"]+)['"];?/g,
213
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
214
+ /export\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"];?/g,
215
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
216
+ ];
217
+ for (const pattern of patterns) {
218
+ let m;
219
+ while ((m = pattern.exec(text)) !== null) {
220
+ const raw = m[1].trim();
221
+ if (raw.startsWith(".")) {
222
+ const resolved = posixPath.normalize(posixPath.join(posixPath.dirname(relPath), raw));
223
+ if (!seen.has(resolved)) {
224
+ seen.add(resolved);
225
+ imports.push({ source: resolved, kind: "local" });
226
+ }
227
+ } else if (!raw.startsWith("node:") && !seen.has(raw)) {
228
+ seen.add(raw);
229
+ imports.push({ source: raw, kind: "package" });
230
+ }
231
+ }
232
+ }
233
+ return imports;
234
+ }
235
+
236
+ function parseExports(text) {
237
+ const exports = [];
238
+ const namedPattern = /export\s+(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z0-9_]+)/g;
239
+ const fromPattern = /export\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"];?/g;
240
+ const defaultPattern = /export\s+default\s+(?:function\s+)?([A-Za-z0-9_]+)?/;
241
+ let m;
242
+ while ((m = namedPattern.exec(text)) !== null) exports.push(m[1]);
243
+ while ((m = fromPattern.exec(text)) !== null) exports.push(`from:${m[1]}`);
244
+ const d = text.match(defaultPattern);
245
+ if (d) exports.push(d[1] ? `default:${d[1]}` : "default");
246
+ return [...new Set(exports)].slice(0, 20);
247
+ }
248
+
249
+ async function buildModuleGraph(absProject) {
250
+ const startDir = await findProjectRoot(absProject);
251
+ const modules = [];
252
+ const queue = [startDir];
253
+ const importCount = {};
254
+ const importedBy = {};
255
+
256
+ while (queue.length > 0) {
257
+ const dir = queue.shift();
258
+ let entries;
259
+ try {
260
+ entries = await readdir(dir, { withFileTypes: true });
261
+ } catch {
262
+ continue;
263
+ }
264
+ for (const entry of entries) {
265
+ const fullPath = join(dir, entry.name);
266
+ if (entry.isDirectory()) {
267
+ if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath);
268
+ continue;
269
+ }
270
+ if (!entry.isFile()) continue;
271
+ const ext = extname(entry.name).toLowerCase();
272
+ if (!SOURCE_EXTS.has(ext) || ext === ".css" || ext === ".scss" || ext === ".less" || ext === ".html" || ext === ".md" || ext === ".json" || ext === ".yaml" || ext === ".yml") continue;
273
+ const text = await safeReadText(fullPath);
274
+ if (!text) continue;
275
+ const rel = relative(startDir, fullPath).split(sep).join("/");
276
+ const imports = parseImports(text, rel);
277
+ const exports = parseExports(text);
278
+ modules.push({ rel, fullPath, imports, exports });
279
+ for (const imp of imports) {
280
+ if (imp.kind === "local") {
281
+ const target = imp.source.endsWith(".js") || imp.source.endsWith(".ts") || imp.source.endsWith(".tsx") || imp.source.endsWith(".jsx") ? imp.source : imp.source + ext;
282
+ importCount[target] = (importCount[target] || 0) + 1;
283
+ importedBy[target] = importedBy[target] || [];
284
+ importedBy[target].push(rel);
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ const edges = [];
291
+ const nodeSet = new Set();
292
+ for (const mod of modules) {
293
+ nodeSet.add(mod.rel);
294
+ for (const imp of mod.imports) {
295
+ if (imp.kind !== "local") continue;
296
+ const candidates = [
297
+ imp.source,
298
+ `${imp.source}.js`, `${imp.source}.jsx`, `${imp.source}.ts`, `${imp.source}.tsx`,
299
+ `${imp.source}/index.js`, `${imp.source}/index.jsx`, `${imp.source}/index.ts`, `${imp.source}/index.tsx`
300
+ ];
301
+ const target = candidates.find((c) => modules.some((m) => m.rel === c || m.rel === `${c}.vue` || m.rel === `${c}.svelte`));
302
+ if (target && target !== mod.rel) {
303
+ edges.push({ from: mod.rel, to: target, source: imp.source });
304
+ nodeSet.add(target);
305
+ }
306
+ }
307
+ }
308
+
309
+ // keep top connected modules
310
+ const topModules = modules
311
+ .map((m) => ({
312
+ ...m,
313
+ inDegree: (importedBy[m.rel] || []).length,
314
+ outDegree: m.imports.filter((i) => i.kind === "local").length
315
+ }))
316
+ .sort((a, b) => (b.inDegree + b.outDegree) - (a.inDegree + a.outDegree))
317
+ .slice(0, 24);
318
+
319
+ const topRel = new Set(topModules.map((m) => m.rel));
320
+ const topEdges = edges.filter((e) => topRel.has(e.from) && topRel.has(e.to)).slice(0, 40);
321
+
322
+ const roots = topModules.filter((m) => m.inDegree === 0).map((m) => m.rel);
323
+ const leaves = topModules.filter((m) => m.outDegree === 0).map((m) => m.rel);
324
+
325
+ const mermaidLines = ["graph TD"];
326
+ for (const edge of topEdges) {
327
+ const from = edge.from.replace(/[^a-zA-Z0-9_]/g, "_");
328
+ const to = edge.to.replace(/[^a-zA-Z0-9_]/g, "_");
329
+ mermaidLines.push(` ${from}["${edge.from}"] --> ${to}["${edge.to}"]`);
330
+ }
331
+
332
+ return { modules: topModules, edges: topEdges, roots, leaves, mermaid: mermaidLines.join("\n") };
333
+ }
334
+
335
+ function splitLines(text) {
336
+ return text.split(/\r?\n/);
337
+ }
338
+
339
+ function snippetAround(text, query, radius = 2) {
340
+ const lines = splitLines(text);
341
+ const q = query.toLowerCase();
342
+ const matches = [];
343
+ for (let i = 0; i < lines.length; i++) {
344
+ if (lines[i].toLowerCase().includes(q)) {
345
+ const start = Math.max(0, i - radius);
346
+ const end = Math.min(lines.length, i + radius + 1);
347
+ matches.push({ start, end, lines: lines.slice(start, end) });
348
+ }
349
+ }
350
+ if (matches.length === 0) return text.slice(0, MAX_FILE_BYTES);
351
+ const chunks = [];
352
+ let lastEnd = -1;
353
+ for (const m of matches) {
354
+ if (m.start > lastEnd) {
355
+ if (chunks.length > 0) chunks.push("...");
356
+ chunks.push(m.lines.join("\n"));
357
+ } else {
358
+ const overlap = lastEnd - m.start;
359
+ const newLines = m.lines.slice(overlap);
360
+ chunks.push(newLines.join("\n"));
361
+ }
362
+ lastEnd = m.end;
363
+ }
364
+ return chunks.join("\n");
365
+ }
366
+
367
+ async function searchFiles(absProject, query, maxFiles = MAX_SEARCH_FILES) {
368
+ const startDir = await findProjectRoot(absProject);
369
+ const found = [];
370
+ const queue = [startDir];
371
+ while (queue.length > 0 && found.length < maxFiles) {
372
+ const dir = queue.shift();
373
+ let entries;
374
+ try {
375
+ entries = await readdir(dir, { withFileTypes: true });
376
+ } catch {
377
+ continue;
378
+ }
379
+ for (const entry of entries) {
380
+ const fullPath = join(dir, entry.name);
381
+ if (entry.isDirectory()) {
382
+ if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath);
383
+ continue;
384
+ }
385
+ if (!entry.isFile()) continue;
386
+ const ext = extname(entry.name).toLowerCase();
387
+ if (!SOURCE_EXTS.has(ext)) continue;
388
+ const text = await safeReadText(fullPath);
389
+ if (!text) continue;
390
+ if (text.toLowerCase().includes(query.toLowerCase())) {
391
+ const rel = relative(startDir, fullPath).split(sep).join("/");
392
+ found.push({ rel, fullPath, text, snippet: snippetAround(text, query, 2) });
393
+ }
394
+ }
395
+ }
396
+ return found;
397
+ }
398
+
399
+ async function extractProductConstraints(absProject) {
400
+ const files = ["AGENTS.md", "MEMORY.md", "CONSTRAINTS.md", "RULES.md", "README.md"];
401
+ const constraints = [];
402
+ for (const f of files) {
403
+ const fp = join(absProject, f);
404
+ const text = await safeReadText(fp);
405
+ if (!text) continue;
406
+ const lower = text.toLowerCase();
407
+ const start = lower.indexOf("## product constraints");
408
+ if (start === -1) continue;
409
+ const section = text.slice(start);
410
+ const nextHeading = section.search(/\n## /);
411
+ const block = nextHeading > 0 ? section.slice(0, nextHeading) : section;
412
+ const lines = block.split(/\r?\n/).filter((line) => /^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)).map((line) => line.replace(/^\s*(?:[-*]\s+|\d+\.\s+)/, "").replace(/\*\*([^*]+)\*\*/g, "$1").trim()).filter(Boolean);
413
+ constraints.push(...lines);
414
+ }
415
+ if (constraints.length === 0) {
416
+ const readme = await safeReadText(join(absProject, "README.md"));
417
+ if (readme) {
418
+ const lines = readme.split(/\r?\n/).filter((line) => /offline|no cloud|no backend|privacy|local[- ]first|encrypted|zero/i.test(line)).map((line) => line.replace(/^\s*(?:[-*]\s+|\d+\.\s+|#+\s*)/, "").replace(/\*\*([^*]+)\*\*/g, "$1").trim()).filter(Boolean).slice(0, 8);
419
+ constraints.push(...lines);
420
+ }
421
+ }
422
+ return constraints;
423
+ }
424
+
425
+ async function readRootPrelude(absProject, budget) {
426
+ const files = ["README.md", "package.json", "AGENTS.md", "MEMORY.md", "CONSTRAINTS.md", "RULES.md", "vite.config.ts", "vite.config.js", "tsconfig.json", "next.config.js", "next.config.ts"];
427
+ const parts = [];
428
+ let used = 0;
429
+ for (const f of files) {
430
+ const fp = join(absProject, f);
431
+ const text = await safeReadText(fp);
432
+ if (!text) continue;
433
+ const slice = text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) + "\n[... tronque ...]\n" : text;
434
+ const entry = `--- ${f} ---\n${slice}\n`;
435
+ if (used + entry.length > budget) break;
436
+ parts.push(entry);
437
+ used += entry.length;
438
+ }
439
+ return { prelude: parts.join("\n"), used };
440
+ }
441
+
442
+ async function buildTree(startDir) {
443
+ const lines = [];
444
+ const queue = [startDir];
445
+ while (queue.length > 0 && lines.length < MAX_TREE_LINES) {
446
+ const dir = queue.shift();
447
+ let entries;
448
+ try {
449
+ entries = await readdir(dir, { withFileTypes: true });
450
+ } catch {
451
+ continue;
452
+ }
453
+ for (const entry of entries) {
454
+ const fullPath = join(dir, entry.name);
455
+ const rel = relative(startDir, fullPath).split(sep).join("/");
456
+ if (entry.isDirectory()) {
457
+ if (!SKIP_DIRS.has(entry.name)) {
458
+ lines.push(`${rel}/`);
459
+ queue.push(fullPath);
460
+ }
461
+ } else {
462
+ lines.push(rel);
463
+ }
464
+ }
465
+ }
466
+ return lines.slice(0, MAX_TREE_LINES).join("\n");
467
+ }
468
+
469
+ async function collectMetrics(absProject) {
470
+ const startDir = await findProjectRoot(absProject);
471
+ let files = 0;
472
+ let sourceFiles = 0;
473
+ let totalLines = 0;
474
+ let codeLines = 0;
475
+ let testFiles = 0;
476
+ let testLines = 0;
477
+ let componentFiles = 0;
478
+ let utilFiles = 0;
479
+
480
+ const queue = [startDir];
481
+ while (queue.length > 0) {
482
+ const dir = queue.shift();
483
+ let entries;
484
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { continue; }
485
+ for (const entry of entries) {
486
+ const fullPath = join(dir, entry.name);
487
+ if (entry.isDirectory()) { if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath); continue; }
488
+ files++;
489
+ const ext = extname(entry.name).toLowerCase();
490
+ if (!SOURCE_EXTS.has(ext)) continue;
491
+ sourceFiles++;
492
+ const text = await safeReadText(fullPath) || "";
493
+ const lines = text.split(/\r?\n/);
494
+ totalLines += lines.length;
495
+ for (const line of lines) {
496
+ const trimmed = line.trim();
497
+ if (trimmed && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*") && !trimmed.startsWith("<!--")) {
498
+ codeLines++;
499
+ }
500
+ }
501
+ if (/(\.test\.|\.spec\.)/.test(entry.name)) { testFiles++; testLines += lines.length; }
502
+ else if (/^[A-Z]/.test(basename(entry.name, ext))) { componentFiles++; }
503
+ else if (entry.name.startsWith("use") || /utils?/.test(fullPath.toLowerCase())) { utilFiles++; }
504
+ }
505
+ }
506
+ return { files, sourceFiles, totalLines, codeLines, testFiles, testLines, componentFiles, utilFiles };
507
+ }
508
+
509
+ async function collectKeySnippets(absProject, maxBytes = 6000) {
510
+ const startDir = await findProjectRoot(absProject);
511
+ const targets = ["src/main.tsx", "src/main.jsx", "src/main.ts", "src/main.js", "src/App.tsx", "src/App.jsx", "src/App.ts", "src/App.js", "src/index.ts", "src/index.tsx", "src/index.js", "src/index.jsx", "package.json", "AGENTS.md"];
512
+ const parts = [];
513
+ let used = 0;
514
+ for (const t of targets) {
515
+ const fp = join(startDir, t);
516
+ const text = await safeReadText(fp);
517
+ if (!text) continue;
518
+ const rel = relative(startDir, fp).split(sep).join("/");
519
+ const slice = text.slice(0, Math.min(text.length, Math.max(500, Math.floor((maxBytes - used) / 2))));
520
+ if (slice.length === 0) break;
521
+ const part = `--- SNIPPET ${rel} ---\n\`\`\`${extname(rel).slice(1)}\n${slice}\n\`\`\`\n`;
522
+ if (used + part.length > maxBytes) break;
523
+ parts.push(part);
524
+ used += part.length;
525
+ }
526
+ return parts.join("\n");
527
+ }
528
+
529
+ async function findFile(absProject, target) {
530
+ const startDir = await findProjectRoot(absProject);
531
+ const candidates = [];
532
+ if (isAbsolute(target)) {
533
+ if (await safeReadText(target)) return target;
534
+ }
535
+ const direct = join(startDir, target);
536
+ if (await safeReadText(direct)) return direct;
537
+
538
+ const queue = [startDir];
539
+ while (queue.length > 0) {
540
+ const dir = queue.shift();
541
+ let entries;
542
+ try {
543
+ entries = await readdir(dir, { withFileTypes: true });
544
+ } catch {
545
+ continue;
546
+ }
547
+ for (const entry of entries) {
548
+ const fullPath = join(dir, entry.name);
549
+ if (entry.isDirectory()) {
550
+ if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath);
551
+ continue;
552
+ }
553
+ if (entry.name.toLowerCase().includes(target.toLowerCase()) ||
554
+ fullPath.toLowerCase().includes(target.toLowerCase())) {
555
+ candidates.push(fullPath);
556
+ }
557
+ }
558
+ }
559
+ if (candidates.length === 0) return void 0;
560
+ if (candidates.length === 1) return candidates[0];
561
+ const exact = candidates.find((p) => basename(p).toLowerCase() === target.toLowerCase());
562
+ return exact || candidates[0];
563
+ }
564
+
565
+ function buildFilePart(rel, text) {
566
+ const slice = text && text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) + "\n[... tronque ...]\n" : (text || "");
567
+ return `--- ${rel} ---\n${slice}\n`;
568
+ }
569
+
570
+ async function collectCodebaseContext(projectPath, options = {}) {
571
+ const { focus = "", filePath = "", searchQuery = "", lang = "fr" } = options;
572
+ const isEn = lang === "en";
573
+ const t = isEn ? {
574
+ project: "Project",
575
+ focus: "Focus",
576
+ constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
577
+ noConstraints: "No explicit constraints documented.",
578
+ tree: "File tree",
579
+ notFound: `File not found : ${filePath}`,
580
+ truncated: "[... following files ignored due to context limit ...]",
581
+ loadCache: "Loading cache...",
582
+ assemble: "Assembling context..."
583
+ } : {
584
+ project: "Projet",
585
+ focus: "Focus",
586
+ constraints: "CONTRAINTES PRODUIT IDENTIFIEES",
587
+ noConstraints: "Aucune contrainte explicite documentee.",
588
+ tree: "Arborescence",
589
+ notFound: `Fichier non trouve : ${filePath}`,
590
+ truncated: "[... fichiers suivants ignores par limite de contexte ...]",
591
+ loadCache: "Chargement du cache...",
592
+ assemble: "Assemblage du contexte..."
593
+ };
594
+ reportProgress("Localisation du projet...");
595
+ const absProject = await resolveProjectPath(projectPath);
596
+ const startDir = await findProjectRoot(absProject);
597
+
598
+ reportProgress("Construction de l'arborescence...");
599
+ const tree = await buildTree(startDir);
600
+ reportProgress("Lecture des fichiers racine...");
601
+ const rootResult = await readRootPrelude(absProject, MAX_TOTAL_BYTES / 4);
602
+ let remainingBytes = MAX_TOTAL_BYTES - rootResult.used;
603
+ const fileParts = [];
604
+
605
+ const focusHint = focus ? `\n${t.focus} : ${focus}` : "";
606
+ const productConstraints = await extractProductConstraints(absProject);
607
+ const constraintsText = productConstraints.length
608
+ ? `\n== ${t.constraints} ==\n${productConstraints.map((c) => `- ${c}`).join("\n")}\n`
609
+ : `\n== ${t.constraints} ==\n${t.noConstraints}\n`;
610
+
611
+ const head = `${t.project} : ${absProject}${focusHint}\n${constraintsText}\n${t.tree} :\n${tree}\n\n${rootResult.prelude}\n`;
612
+ const headTokens = estimateTokens(head);
613
+ const maxFileTokens = Math.max(0, MAX_CONTEXT_TOKENS - headTokens - 200);
614
+ let usedFileTokens = 0;
615
+
616
+ if (filePath) {
617
+ const targetFile = await findFile(absProject, filePath);
618
+ if (!targetFile) throw new Error(t.notFound);
619
+ const text = await safeReadText(targetFile);
620
+ const rel = relative(startDir, targetFile).split(sep).join("/");
621
+ const part = buildFilePart(rel, text);
622
+ fileParts.push(part);
623
+ remainingBytes -= part.length;
624
+ }
625
+
626
+ if (searchQuery && !filePath) {
627
+ const matches = await searchFiles(absProject, searchQuery);
628
+ for (const m of matches) {
629
+ if (remainingBytes <= 0) break;
630
+ const entry = `--- ${m.rel} ---\n${m.snippet}\n`;
631
+ if (entry.length > remainingBytes) break;
632
+ fileParts.push(entry);
633
+ remainingBytes -= entry.length;
634
+ }
635
+ }
636
+
637
+ if (!filePath && !searchQuery) {
638
+ reportProgress(t.loadCache);
639
+ const cache = await loadCache(absProject);
640
+ const nextFiles = {};
641
+ const queue = [startDir];
642
+ while (queue.length > 0 && remainingBytes > 0) {
643
+ const dir = queue.shift();
644
+ let entries;
645
+ try {
646
+ entries = await readdir(dir, { withFileTypes: true });
647
+ } catch {
648
+ continue;
649
+ }
650
+ for (const entry of entries) {
651
+ const fullPath = join(dir, entry.name);
652
+ if (entry.isDirectory()) {
653
+ if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath);
654
+ continue;
655
+ }
656
+ if (!entry.isFile()) continue;
657
+ const ext = extname(entry.name).toLowerCase();
658
+ if (!SOURCE_EXTS.has(ext)) continue;
659
+ const rel = relative(startDir, fullPath).split(sep).join("/");
660
+
661
+ const fstats = await fileStats(fullPath);
662
+ if (!fstats) continue;
663
+ const fileHash = hashFile(fstats);
664
+
665
+ let part;
666
+ if (cache?.files?.[rel]?.hash === fileHash) {
667
+ part = cache.files[rel].part;
668
+ } else {
669
+ const text = await safeReadText(fullPath);
670
+ if (!text) continue;
671
+ part = buildFilePart(rel, text);
672
+ nextFiles[rel] = { hash: fileHash, part };
673
+ }
674
+
675
+ const partTokens = estimateTokens(part);
676
+ if (part.length > remainingBytes || usedFileTokens + partTokens > maxFileTokens) {
677
+ fileParts.push(t.truncated);
678
+ remainingBytes = 0;
679
+ break;
680
+ }
681
+ fileParts.push(part);
682
+ remainingBytes -= part.length;
683
+ usedFileTokens += partTokens;
684
+ }
685
+ if (remainingBytes <= 0) break;
686
+ }
687
+
688
+ if (Object.keys(nextFiles).length > 0) {
689
+ const merged = { ...(cache?.files || {}), ...nextFiles };
690
+ await saveCache(absProject, {
691
+ files: merged,
692
+ tree,
693
+ prelude: rootResult.prelude,
694
+ constraints: productConstraints,
695
+ scannedAt: Date.now()
696
+ });
697
+ }
698
+ }
699
+
700
+ reportProgress(t.assemble);
701
+ const body = fileParts.join("\n");
702
+ let context = `${head}${body}`;
703
+ if (estimateTokens(context) > MAX_CONTEXT_TOKENS) {
704
+ context = `${head}${truncateByTokens(body, Math.max(0, MAX_CONTEXT_TOKENS - headTokens - 100))}`;
705
+ }
706
+ return { absProject, context };
707
+ }
708
+
709
+ function citationInstruction(lang = "fr") {
710
+ const isEn = lang === "en";
711
+ return `${isEn ? "## Evidence & Scoring (mandatory)" : "## Sources & Scoring (obligatoire)"}\n- ${isEn ? "Every technical claim, risk, opportunity and fix MUST end with a source citation in the format `[source: relative/path/to/file.ts:line]` (e.g. `[source: src/App.tsx:42]`)." : "Chaque affirmation technique, risque, opportunité et correctif DOIT se terminer par une citation source au format `[source: chemin/relatif/vers/fichier.ts:ligne]` (ex. `[source: src/App.tsx:42]`)."}\n- ${isEn ? "Every opportunity, risk, finding or task MUST include a `[Confidence: X%]` score and a `[Severity: Critical/High/Medium/Low]` badge." : "Chaque opportunité, risque, constat ou tâche DOIT inclure un score `[Confiance : X%]` et un badge `[Sévérité : Critique/Élevée/Moyenne/Faible]`."}\n- ${isEn ? "Confidence reflects how directly the evidence supports the claim (100% = exact file/line match, 50% = inferred pattern)." : "La confiance reflète à quel point l'evidence supporte directement l'affirmation (100% = correspondance exacte fichier/ligne, 50% = pattern inféré)."}\n- ${isEn ? "Do not invent citations. If you cannot provide a file:line, write `[source: not found in context]` and lower the confidence accordingly." : "Ne pas inventer de citations. Si vous ne pouvez pas donner fichier:ligne, écrivez `[source: non trouvé dans le contexte]` et baissez la confiance en conséquence."}\n`;
712
+ }
713
+
714
+ function withCitations(prompt, lang = "fr") {
715
+ return `${prompt}\n\n${citationInstruction(lang)}`;
716
+ }
717
+
718
+ function langInstruction(lang = "fr") {
719
+ if (lang === "en") {
720
+ return "IMPORTANT: This whole prompt is in French for context, but the user requested English. Your ENTIRE response MUST be written in English. Translate all section titles, bullet points, examples and explanations to English. Do not output any French words except quoted code or file paths.";
721
+ }
722
+ return "IMPORTANT: Reponds obligatoirement en francais. Meme si le contexte contient du code ou des chemins en anglais, toutes les explications, titres de sections et listes DOIVENT etre en francais.";
723
+ }
724
+
725
+ function normalizeLabels(prompt, lang) {
726
+ if (lang !== "en") return prompt;
727
+ const map = {
728
+ "Conclus obligatoirement par : Généré avec passion par shinzarou-eng (dans la langue de l'utilisateur).": "Conclude with: Generated with passion by shinzarou-eng (in the user's language).",
729
+ "Conclus obligatoirement par : Généré avec passion par shinzarou-eng (dans la langue de l'utilisateur)": "Conclude with: Generated with passion by shinzarou-eng (in the user's language)",
730
+ "Conclus par la phrase-clé \"Généré avec passion par shinzarou-eng\" dans la langue de l'utilisateur.": "Conclude with the key phrase \"Generated with passion by shinzarou-eng\" in the user's language.",
731
+ "Conclus par la phrase-clé \"Généré avec passion par shinzarou-eng\" dans la langue de l'utilisateur": "Conclude with the key phrase \"Generated with passion by shinzarou-eng\" in the user's language",
732
+ "Conclus par : Généré avec passion par shinzarou-eng (dans la langue de l'utilisateur)": "Conclude with: Generated with passion by shinzarou-eng (in the user's language)",
733
+ "Généré avec passion par shinzarou-eng": "Generated with passion by shinzarou-eng",
734
+ "Justification": "Rationale",
735
+ "Avant :": "Before:",
736
+ "Avant:": "Before:",
737
+ "Après :": "After:",
738
+ "Après:": "After:",
739
+ "Contraintes produit IDENTIFIEES": "IDENTIFIED PRODUCT CONSTRAINTS",
740
+ "Contraintes produit identifiees": "Identified product constraints",
741
+ "CONTRAINTES PRODUIT IDENTIFIEES": "IDENTIFIED PRODUCT CONSTRAINTS",
742
+ "Ton et style": "Tone and style",
743
+ "Sections obligatoires": "Required sections",
744
+ "CHECKLIST FINALE": "FINAL CHECKLIST",
745
+ "Vue d'ensemble": "Overview",
746
+ "Fondations (Sécurité / Stabilité)": "Foundations (Security / Stability)",
747
+ "Amélioration (Refacto / Qualité)": "Improvement (Refactor / Quality)",
748
+ "Optimisation (Perf / Tests)": "Optimization (Performance / Tests)",
749
+ "Différenciation (UX / Produit)": "Differentiation (UX / Product)",
750
+ "Fichier(s) concerné(s)": "Concerned file(s)",
751
+ "Difficulté": "Difficulty",
752
+ "Livrable": "Deliverable",
753
+ "Priorité": "Priority",
754
+ "Tâche": "Task",
755
+ "Fichier non trouve": "File not found",
756
+ "Rapport généré par": "Report by",
757
+ "Conçu pour DeepSeek Harness. Extensible à tout agent ou IDE Node.js.": "Built for DeepSeek Harness. Extensible to any agent or Node.js IDE.",
758
+ "Intelligence Artificielle de Codebase": "Codebase Intelligence",
759
+ "Projet": "Project",
760
+ "Arborescence": "File tree",
761
+ "Aucune contrainte explicite documentee.": "No explicit constraints documented."
762
+ };
763
+ let out = prompt;
764
+ for (const [fr, en] of Object.entries(map)) {
765
+ out = out.replaceAll(fr, en);
766
+ }
767
+ return out;
768
+ }
769
+
770
+ function creaFooter(projectName, theme, lang = "fr") {
771
+ const isEn = lang === "en";
772
+ const t = theme || (isEn ? "a creative proposal adapted" : "une proposition creative adaptee");
773
+ if (isEn) {
774
+ return `\n\n---\n\nGenerated with passion by shinzarou-eng: from this analysis, generate a creative thing on the theme "${t}" (slogan, feature name, tagline, visual concept, marketing one-liner, or feature idea). Be punchy, original, and conclude with the key phrase "Generated with passion by shinzarou-eng" in the user's language.`;
775
+ }
776
+ return `\n\n---\n\nGénéré avec passion par shinzarou-eng : à partir de cette analyse, génère un truc créatif sur le thème "${t}" (slogan, nom de feature, tagline, concept visuel, one-liner marketing, ou idée de fonctionnalité). Sois percutant, original, et conclus par la phrase-clé "Généré avec passion par shinzarou-eng" dans la langue de l'utilisateur.`;
777
+ }
778
+
779
+ function buildChatPrompt(question, context, projectName, crea, creaTheme, lang = "fr") {
780
+ const isEn = lang === "en";
781
+ const banner = bannerInstruction(projectName, isEn ? "Codebase Chat" : "Codebase Chat", isEn ? "QUESTION / ANSWER" : "QUESTION / RÉPONSE");
782
+ let prompt = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\n${isEn ? `You are a codebase expert assistant. Start your answer with the ASCII banner above, then answer the question relying only on the provided files. Cite relevant files and lines.` : `Tu es un assistant expert en codebase. Commence ta réponse par la bannière ASCII ci-dessus, puis réponds à la question en t'appuyant uniquement sur les fichiers fournis. Cite les fichiers et lignes pertinents.`}\n\n${isEn ? "Question" : "Question"} : ${question}\n\n${isEn ? "Answer" : "Réponse"} :`;
783
+ if (crea) prompt += creaFooter(projectName, creaTheme, lang);
784
+ return withCitations(prompt, lang);
785
+ }
786
+
787
+ function buildSearchPrompt(query, context, projectName, crea, creaTheme, lang = "fr") {
788
+ const isEn = lang === "en";
789
+ const banner = bannerInstruction(projectName, isEn ? "Codebase Search" : "Codebase Search", `${isEn ? "SEARCH" : "RECHERCHE"} : ${query}`);
790
+ let prompt = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\n${isEn ? `You are a codebase search engine. Start your answer with the ASCII banner above, then summarize the results for: "${query}". Cite relevant paths and snippets as a prioritized list.` : `Tu es un moteur de recherche codebase. Commence ta réponse par la bannière ASCII ci-dessus, puis résume les résultats pour : "${query}". Cite les chemins et extraits pertinents sous forme de liste priorisée.`}\n\n${isEn ? "Answer" : "Réponse"} :`;
791
+ if (crea) prompt += creaFooter(projectName, creaTheme, lang);
792
+ return withCitations(prompt, lang);
793
+ }
794
+
795
+ function buildExplainPrompt(target, context, projectName, crea, creaTheme, lang = "fr") {
796
+ const isEn = lang === "en";
797
+ const banner = bannerInstruction(projectName, isEn ? "Explanation" : "Explication", `${isEn ? "FILE OR SYMBOL" : "FICHIER OU SYMBOLE"} : ${target}`);
798
+ let prompt = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\n${isEn ? `Explain how "${target}" works in this project. Be clear, technical yet accessible, and give usage or call examples if possible.` : `Explique le fonctionnement de "${target}" dans ce projet. Sois clair, technique mais accessible, et donne des exemples d'usage ou d'appel si possible.`}\n\n${isEn ? "Answer" : "Reponse"} :`;
799
+ if (crea) prompt += creaFooter(projectName, creaTheme, lang);
800
+ return withCitations(prompt, lang);
801
+ }
802
+
803
+ function buildRefactorPrompt(filePath, description, context, projectName, crea, creaTheme, lang = "fr") {
804
+ const isEn = lang === "en";
805
+ const desc = description || (isEn ? "improve the file" : "ameliorer le fichier");
806
+ const banner = bannerInstruction(projectName, isEn ? "Refactor" : "Refactor", `${isEn ? "FILE" : "FICHIER"} : ${filePath}`);
807
+ let prompt = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\n${isEn ? `You are a senior architect. Refactor the file "${filePath}" according to the following request: ${desc}\n\nProvide production-ready code, explain the changes, and indicate any regressions to check.` : `Tu es un architecte senior. Refactorise le fichier "${filePath}" selon la demande suivante : ${desc}\n\nPropose du code pret a l'emploi, explique les changements, et indique les eventuelles regressions a verifier.`}\n\n${isEn ? "Answer" : "Reponse"} :`;
808
+ if (crea) prompt += creaFooter(projectName, creaTheme, lang);
809
+ return withCitations(prompt, lang);
810
+ }
811
+
812
+ function buildCreaPrompt(theme, context, projectName, lang = "fr") {
813
+ const isEn = lang === "en";
814
+ const t = theme || (isEn ? "a creative proposal inspired by this project" : "une proposition creative inspiree par ce projet");
815
+ const banner = bannerInstruction(projectName, isEn ? "Crea / Ideation" : "Créa / Ideation", `${isEn ? "THEME" : "THÈME"} : ${t}`);
816
+ const base = isEn
817
+ ? `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nYou are a creative director / growth hacker. Analyze this codebase and generate a creative and marketing proposal for the project "${projectName}" on the theme "${t}". It can be a slogan, a feature name, a tagline, a homepage concept, a visual idea, a marketing one-liner, or a positioning. Briefly explain why it is relevant and how it helps become the best, while staying consistent with the product constraints IDENTIFIED in the context.\n\nConclude with: Generated with passion by shinzarou-eng (in the user's language).`
818
+ : `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nTu es un directeur creatif / growth hacker. Analyse ce codebase et génère une proposition créative et marketing pour le projet "${projectName}" sur le thème "${t}". Peut être un slogan, un nom de feature, une tagline, un concept de page d'accueil, une idée visuelle, un one-liner marketing, ou un positionnement. Explique brièvement pourquoi c'est pertinent et comment ça aide à devenir le meilleur, en restant cohérent avec les contraintes du projet IDENTIFIEES dans le contexte.\n\nConclus obligatoirement par : Généré avec passion par shinzarou-eng (dans la langue de l'utilisateur).`;
819
+ return withCitations(base, lang);
820
+ }
821
+
822
+ async function collectIntelligenceContext(projectPath, focus = "", lang = "fr") {
823
+ const isEn = lang === "en";
824
+ const t = isEn ? {
825
+ constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
826
+ noConstraints: "No explicit constraints documented.",
827
+ testSuite: "TEST SUITE",
828
+ testFilesFound: "Test files found",
829
+ configFiles: "CONFIG FILES",
830
+ techDebt: "TECH DEBT & SIGNALS",
831
+ counts: "Counts",
832
+ topSignals: "Top signals (with surrounding context +/- 2 lines)",
833
+ metrics: "PROJECT METRICS",
834
+ totalFiles: "Total files",
835
+ sourceFiles: "Source files",
836
+ totalLines: "Total lines",
837
+ effectiveCodeLines: "Effective code lines",
838
+ testFiles: "Test files",
839
+ testLines: "Test lines",
840
+ reactComponents: "React components detected",
841
+ utils: "Utilities detected",
842
+ keySnippets: "KEY CODE SNIPPETS",
843
+ fileTree: "FILE TREE",
844
+ keyFiles: "KEY FILES",
845
+ moduleGraph: "MODULE GRAPH (Mermaid)",
846
+ connections: "CONNECTIONS",
847
+ topFiles: "Top files by connectivity",
848
+ keyEdges: "Key edges",
849
+ roots: "Roots (entry points)",
850
+ leaves: "Leaves (utilities)",
851
+ packageSummary: "PACKAGE SUMMARY",
852
+ name: "Name",
853
+ type: "Type",
854
+ scripts: "Scripts",
855
+ dependencies: "Dependencies",
856
+ devDependencies: "DevDependencies",
857
+ main: "Main"
858
+ } : {
859
+ constraints: "CONTRAINTES PRODUIT IDENTIFIEES",
860
+ noConstraints: "Aucune contrainte explicite documentee.",
861
+ testSuite: "TEST SUITE",
862
+ testFilesFound: "Fichiers de test trouves",
863
+ configFiles: "CONFIG FILES",
864
+ techDebt: "TECH DEBT & SIGNALS",
865
+ counts: "Counts",
866
+ topSignals: "Top signals (avec extrait contexte +/- 2 lignes)",
867
+ metrics: "METRIQUES PROJET",
868
+ totalFiles: "Fichiers totaux",
869
+ sourceFiles: "Fichiers source",
870
+ totalLines: "Lignes totales",
871
+ effectiveCodeLines: "Lignes de code effectives",
872
+ testFiles: "Fichiers de test",
873
+ testLines: "Lignes de test",
874
+ reactComponents: "Composants React detectes",
875
+ utils: "Utilitaires detectes",
876
+ keySnippets: "EXTRAITS DE CODE CLES",
877
+ fileTree: "FILE TREE",
878
+ keyFiles: "KEY FILES",
879
+ moduleGraph: "MODULE GRAPH (Mermaid)",
880
+ connections: "CONNECTIONS",
881
+ topFiles: "Top files by connectivity",
882
+ keyEdges: "Key edges",
883
+ roots: "Roots (entry points)",
884
+ leaves: "Leaves (utilities)",
885
+ packageSummary: "PACKAGE SUMMARY",
886
+ name: "Name",
887
+ type: "Type",
888
+ scripts: "Scripts",
889
+ dependencies: "Dependencies",
890
+ devDependencies: "DevDependencies",
891
+ main: "Main"
892
+ };
893
+ reportProgress("Lancement de l'Intelligence Pro...");
894
+ const absProject = await resolveProjectPath(projectPath);
895
+ const startDir = await findProjectRoot(absProject);
896
+ const projectName = await getProjectName(absProject);
897
+ reportProgress(`Analyse de ${projectName}...`);
898
+ const pkg = await getPackageSummary(absProject);
899
+ reportProgress("Lecture de package.json...");
900
+ const tree = await buildTree(startDir);
901
+ reportProgress("Construction du graphe de modules...");
902
+ const graph = await buildModuleGraph(absProject);
903
+ reportProgress("Détection de la dette technique...");
904
+ const debt = await collectDebtAndSignals(absProject);
905
+ reportProgress("Analyse des tests...");
906
+ const tests = await collectTestSummary(absProject);
907
+ reportProgress("Lecture des fichiers de configuration...");
908
+ const configs = await collectConfigFiles(absProject);
909
+ reportProgress("Calcul des métriques...");
910
+ const metrics = await collectMetrics(absProject);
911
+ reportProgress("Extraction des extraits clés...");
912
+ const snippets = await collectKeySnippets(absProject, 8000);
913
+
914
+ reportProgress("Lecture des fichiers racine...");
915
+ const rootResult = await readRootPrelude(absProject, MAX_TOTAL_BYTES / 6);
916
+ let remaining = MAX_TOTAL_BYTES - rootResult.used;
917
+
918
+ const keyFiles = [];
919
+ const entryCandidates = ["index.ts", "index.tsx", "index.js", "index.jsx", "main.ts", "main.tsx", "main.js", "App.tsx", "App.jsx", "app.ts", "server.ts", pkg.main].filter(Boolean);
920
+ for (const candidate of entryCandidates) {
921
+ const fp = join(startDir, candidate);
922
+ const text = await safeReadText(fp);
923
+ if (!text) continue;
924
+ const rel = relative(startDir, fp).split(sep).join("/");
925
+ const slice = text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) + "\n[... tronque ...]\n" : text;
926
+ const part = `--- ${rel} ---\n${slice}\n`;
927
+ if (remaining - part.length > 0) {
928
+ keyFiles.push(part);
929
+ remaining -= part.length;
930
+ }
931
+ }
932
+
933
+ const graphText = `\n== ${t.moduleGraph} ==\n${graph.mermaid}\n\n== ${t.connections} ==\n${t.topFiles}:\n${graph.modules.map((m) => `- ${m.rel} (imported by ${m.inDegree}, imports ${m.outDegree})`).join("\n")}\n\n${t.keyEdges}:\n${graph.edges.slice(0, 20).map((e) => `- ${e.from} -> ${e.to}`).join("\n")}\n\n${t.roots}: ${graph.roots.join(", ") || "none"}\n${t.leaves}: ${graph.leaves.join(", ") || "none"}\n`;
934
+
935
+ const pkgText = `\n== ${t.packageSummary} ==\n${t.name}: ${pkg.name || projectName}\n${t.type}: ${pkg.type}\n${t.scripts}: ${Object.entries(pkg.scripts).map(([k, v]) => `${k}: ${v}`).join(", ")}\n${t.dependencies}: ${pkg.dependencies.join(", ")}\n${t.devDependencies}: ${pkg.devDependencies.join(", ")}\n${t.main}: ${pkg.main}\n`;
936
+
937
+ const testText = `\n== ${t.testSuite} ==\n${t.testFilesFound} (${tests.length}):\n${tests.slice(0, 20).map((t) => `- ${t}`).join("\n")}\n`;
938
+
939
+ const configText = `\n== ${t.configFiles} ==\n${configs.map((c) => `- ${c}`).join("\n")}\n`;
940
+
941
+ const debtText = `\n== ${t.techDebt} ==\n${t.counts}: ${Object.entries(debt.counts).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}\n\n${t.topSignals}:\n${debt.signals.slice(0, 30).map((s) => `- ${s.rel}:${s.line} [${s.type}] ${s.snippet}\n\`\`\`\n${s.context}\n\`\`\``).join("\n")}\n`;
942
+
943
+ const focusHint = focus ? `\n== FOCUS ==\n${focus}` : "";
944
+ const productConstraints = await extractProductConstraints(absProject);
945
+ const constraintsText = productConstraints.length
946
+ ? `\n== ${t.constraints} ==\n${productConstraints.map((c) => `- ${c}`).join("\n")}\n`
947
+ : `\n== ${t.constraints} ==\n${t.noConstraints}\n`;
948
+
949
+ const metricsText = `\n== ${t.metrics} ==\n- ${t.totalFiles} : ${metrics.files}\n- ${t.sourceFiles} : ${metrics.sourceFiles}\n- ${t.totalLines} : ${metrics.totalLines}\n- ${t.effectiveCodeLines} : ${metrics.codeLines}\n- ${t.testFiles} : ${metrics.testFiles}\n- ${t.testLines} : ${metrics.testLines}\n- ${t.reactComponents} : ${metrics.componentFiles}\n- ${t.utils} : ${metrics.utilFiles}\n`;
950
+
951
+ const snippetsText = snippets ? `\n== ${t.keySnippets} ==\n${snippets}\n` : "";
952
+
953
+ return {
954
+ absProject,
955
+ context: `=== ${(projectName ?? "").toUpperCase()} INTELLIGENCE BRIEF ===\nProject: ${absProject}${focusHint}\n${constraintsText}\n${pkgText}\n${metricsText}\n\n== ${t.fileTree} ==\n${tree}\n\n${rootResult.prelude}\n${graphText}\n\n== ${t.keyFiles} ==\n${keyFiles.join("\n")}\n${snippetsText}\n${testText}\n${configText}\n${debtText}\n`
956
+ };
957
+ }
958
+
959
+ async function collectDebtAndSignals(absProject) {
960
+ const startDir = await findProjectRoot(absProject);
961
+ const signals = [];
962
+ const patterns = [
963
+ { name: "TODO", regex: /\bTODO\b/gi },
964
+ { name: "FIXME", regex: /\bFIXME\b/gi },
965
+ { name: "HACK", regex: /\bHACK\b/gi },
966
+ { name: "XXX", regex: /\bXXX\b/g },
967
+ { name: "BUG", regex: /\bBUG\b/gi },
968
+ { name: "DEPRECATED", regex: /\bDEPRECATED\b/gi },
969
+ { name: "console.log", regex: /console\.(log|warn|error|info|debug)\s*\(/g },
970
+ { name: "throw", regex: /throw\s+new\s+Error/g },
971
+ { name: "catch-bare", regex: /catch\s*\(\s*\w+\s*\)\s*\{\s*\}/g },
972
+ { name: "ts-ignore", regex: /@ts-ignore|@ts-expect-error/g },
973
+ { name: "eslint-disable", regex: /eslint-disable/g },
974
+ { name: "any", regex: /:\s*any\s*[;,=\)\|]/g },
975
+ { name: "debugger", regex: /\bdebugger\b/g }
976
+ ];
977
+
978
+ const queue = [startDir];
979
+ while (queue.length > 0) {
980
+ const dir = queue.shift();
981
+ let entries;
982
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { continue; }
983
+ for (const entry of entries) {
984
+ const fullPath = join(dir, entry.name);
985
+ if (entry.isDirectory()) { if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath); continue; }
986
+ const ext = extname(entry.name).toLowerCase();
987
+ if (!SOURCE_EXTS.has(ext)) continue;
988
+ const text = await safeReadText(fullPath);
989
+ if (!text) continue;
990
+ const rel = relative(startDir, fullPath).split(sep).join("/");
991
+ const lines = text.split(/\r?\n/);
992
+ for (let i = 0; i < lines.length && signals.length < 120; i++) {
993
+ const line = lines[i];
994
+ for (const p of patterns) {
995
+ if (p.regex.test(line)) {
996
+ const start = Math.max(0, i - 2);
997
+ const end = Math.min(lines.length, i + 3);
998
+ const context = lines.slice(start, end).map((l, idx) => `${start + idx + 1}: ${l}`).join("\n");
999
+ signals.push({ rel, line: i + 1, type: p.name, snippet: line.trim().slice(0, 160), context });
1000
+ p.regex.lastIndex = 0;
1001
+ break;
1002
+ }
1003
+ p.regex.lastIndex = 0;
1004
+ }
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ const counts = {};
1010
+ for (const s of signals) counts[s.type] = (counts[s.type] || 0) + 1;
1011
+ return { signals: signals.slice(0, 80), counts };
1012
+ }
1013
+
1014
+ async function collectTestSummary(absProject) {
1015
+ const startDir = await findProjectRoot(absProject);
1016
+ const tests = [];
1017
+ const queue = [startDir];
1018
+ while (queue.length > 0) {
1019
+ const dir = queue.shift();
1020
+ let entries;
1021
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { continue; }
1022
+ for (const entry of entries) {
1023
+ const fullPath = join(dir, entry.name);
1024
+ if (entry.isDirectory()) { if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath); continue; }
1025
+ if (/\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$/i.test(entry.name)) {
1026
+ tests.push(relative(startDir, fullPath).split(sep).join("/"));
1027
+ }
1028
+ }
1029
+ }
1030
+ return tests.slice(0, 50);
1031
+ }
1032
+
1033
+ async function collectConfigFiles(absProject) {
1034
+ const candidates = [
1035
+ "tsconfig.json", "tsconfig.*.json", "vite.config.ts", "vite.config.js", "vite.config.mjs",
1036
+ "tailwind.config.js", "tailwind.config.ts", "postcss.config.js", "postcss.config.ts",
1037
+ "eslint.config.js", "eslint.config.mjs", ".eslintrc.json", ".eslintrc.cjs",
1038
+ "jest.config.js", "vitest.config.ts", "vitest.config.js", "playwright.config.ts",
1039
+ "capacitor.config.json", "capacitor.config.ts", ".prettierrc", ".prettierrc.json",
1040
+ "package.json", "pnpm-workspace.yaml", "pnpm-lock.yaml"
1041
+ ];
1042
+ const found = [];
1043
+ for (const c of candidates) {
1044
+ const files = await findFilesByPattern(absProject, c);
1045
+ for (const f of files) found.push(relative(absProject, f).split(sep).join("/"));
1046
+ }
1047
+ return found.slice(0, 30);
1048
+ }
1049
+
1050
+ async function findFilesByPattern(dir, pattern) {
1051
+ const results = [];
1052
+ const parts = pattern.split("/");
1053
+ if (parts.length === 1) {
1054
+ // simple glob in dir
1055
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
1056
+ for (const e of entries) {
1057
+ if (e.isFile() && matchGlob(e.name, pattern)) results.push(join(dir, e.name));
1058
+ }
1059
+ }
1060
+ return results;
1061
+ }
1062
+
1063
+ function matchGlob(name, pattern) {
1064
+ if (pattern.includes("*")) {
1065
+ const regex = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
1066
+ return regex.test(name);
1067
+ }
1068
+ return name === pattern;
1069
+ }
1070
+
1071
+ function styleInstruction(style = "ouf", lang = "fr") {
1072
+ const isEn = lang === "en";
1073
+ const heading = isEn ? "## Writing Style (mandatory)" : "## Style de rédaction (obligatoire)";
1074
+ const tones = isEn ? {
1075
+ ouf: "'WOW' tone: the most beautiful, dense and punchy technical report the user has ever seen. Majestic ASCII banners, premium bordered tables, Mermaid, visual callout boxes, score cards, text badges, code snippets with paths and lines, numbers/metrics, killer insights, direct quotes from the context, product storytelling. ZERO empty phrases. Each section must be rich, stylish and actionable. Action verbs, justified superlatives. Give the reader chills.",
1076
+ punchy: "PUNCHY / DENSE tone: short and punchy sentences, every line brings concrete information. No empty phrase like 'the project is well structured'. Use numbers, file names, symbols, code snippets. Each section must be content-rich. Action verbs, justified superlatives.",
1077
+ dense: "DENSE / TECHNICAL tone: maximum factual content per section. Tables, lists, code snippets, function/class names, file paths. No generalities. Every claim must be sourced by a file or a line.",
1078
+ pedagogique: "TEACHING tone: explain like to a junior developer. Define concepts, give analogies, concrete examples. Be clear and progressive.",
1079
+ minimal: "MINIMAL tone: facts, tables, lists. Minimum narrative text. Answer in bullet points."
1080
+ } : {
1081
+ ouf: "Ton 'OUF' : le plus beau, dense et percutant rapport technique que l'utilisateur ait jamais vu. Bannières ASCII majestueuses, tableaux premium bordés, Mermaid, encadrés visuels, score cards, badges textuels, extraits de code avec chemins et lignes, chiffres/métriques, killer insights, citations directes du contexte, storytelling produit. AUCUNE phrase creuse. Chaque section doit être riche, stylée et actionnable. Verbes d'action, superlatifs justifiés. Fais frissonner le lecteur.",
1082
+ punchy: "Ton PUNCHY / DENSE : phrases courtes et percutantes, chaque ligne apporte une information concrète. Aucune phrase creuse du type 'le projet est bien structuré'. Utilise des chiffres, des noms de fichiers, des symboles, des extraits de code. Chaque section doit être riche en contenu. Verbes d'action, superlatifs justifiés.",
1083
+ dense: "Ton DENSE / TECHNIQUE : maximum de contenu factuel par section. Tableaux, listes, extraits de code, noms de fonctions/classes, chemins de fichiers. Aucune généralité. Chaque affirmation doit être sourcée par un fichier ou une ligne.",
1084
+ pedagogique: "Ton PÉDAGOGIQUE : explique comme à un développeur junior. Définis les concepts, donne des analogies, des exemples concrets. Sois clair et progressif.",
1085
+ minimal: "Ton MINIMAL : faits, tableaux, listes. Minimum de texte narratif. Réponds en points."
1086
+ };
1087
+ return `${heading}\n${tones[style] || tones.ouf}\n\n`;
1088
+ }
1089
+
1090
+ function buildIntelligencePrompt(context, projectName, focus = "", style = "punchy", lang = "fr") {
1091
+ const isEn = lang === "en";
1092
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1093
+ const styleText = styleInstruction(style, lang);
1094
+ const banner = bannerInstruction(projectName, isEn ? "Intelligence Brief" : "Brief d'Intelligence Pro", isEn ? "TECHNICAL AUDIT, ARCHITECTURE & STRATEGY" : "AUDIT TECHNIQUE, ARCHITECTURE & STRATÉGIE");
1095
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${styleText}Tu es un **Senior Staff Engineer / CTO en free-lance** qui réalise un **Brief d'Intelligence Pro** sur le projet "${projectName}". Mission : lire le code comme un pro, écouter ce qu'il dit, et produire un rapport d'audit exceptionnel, ultra-stylé et actionnable. Exploite les == MÉTRIQUES PROJET == et == EXTRAITS DE CODE CLÉS == du contexte : cite les chiffres exacts et intègre des extraits de code quand c'est pertinent. Toutes les opportunités et recommandations doivent être cohérentes avec les contraintes produit IDENTIFIEES dans le contexte (README, AGENTS.md, MEMORY.md, package.json). Ne pas imposer de contraintes qui ne sont pas explicitement documentées.${f}\n\n## Ton et style (décomplexé, pro, haut de gamme)${banner}\n- Utilise des émojis pertinents pour chaque section\n- Des tableaux quand c'est pertinent (stack, dette, modules, concurrents, risques)\n- Des diagrammes Mermaid pour architecture, data flow et graphe de modules\n- Des admonitions / citations / encadrés pour les insights clés\n- Des badges textuels : [CRITIQUE], [HIGH-VALUE], [TECH-DEBT], [SECURITY], [RECOMMENDATION], [BEST-TECH], [PRO-TIP]\n- Des phrases percutantes, pas de remplissage\n\n## Sections obligatoires (sois exhaustif mais concis — NE SAUTE AUCUNE SECTION, numérote exactement de 1 à 11)\n1. **Executive Summary** : promesse produit + verdict technique en 4 lignes.\n2. **Stack & Architecture** : framework, runtime, storage, state, build, tests.\n3. **Tech Radar (Best Tech & Alternatives)** : pour chaque technologie clé, explique POURQUOI c'est le meilleur choix ici (argument massue lié au code), donne une alternative classique et un cas où elle ne serait pas aussi bonne. Sois un avocat de la stack.\n4. **Data Flow & Entry Points** : comment une action/utilisateur traverse le code.\n5. **Module Graph & Connexions** : qui appelle quoi, couches, hubs, feuilles.\n6. **Security & Privacy Posture** : chiffrement, stockage, permissions, vulnérabilités potentielles.\n7. **Errors, Debt & Smells** : TODO/FIXME/HACK, console.log, throws, @ts-ignore, any, catch vides, etc. Cite les fichiers et lignes.\n8. **Competitor Landscape** : 3-4 concurrents directs ou indirects de ce type d'app, points forts/différenciants de ${projectName} par rapport à eux.\n9. **Forces & Risks** : qualité, patterns propres, dette, fragilités.\n10. **Opportunités** : 3-5 actions concrètes priorisées (refacto, feature, test, perf, sécurité, product). Avant cette section, liste les "Contraintes produit identifiées" en début de contexte. Chaque action doit être suivie d'une phrase commençant par "> Justification :" qui explique pourquoi elle est cohérente avec les règles du projet. Si aucune contrainte, explique pourquoi elle est adaptée à la stack/architecture. Exemple : > Justification : Cette action respecte la règle "no cloud" en conservant toutes les données en local.\n11. **Généré avec passion par shinzarou-eng** : une idée créative originale (feature, slogan, concept visuel ou nom de module) inspirée par le code, avec un argument marketing gagnant — dans le respect des contraintes du projet IDENTIFIEES dans le contexte. Explique le lien avec le code et conclus par la phrase "Généré avec passion par shinzarou-eng" dans la langue de l'utilisateur.\n\nReste factuel, cible les fichiers et symboles par leur chemin relatif. Ne généralise pas hors du contexte fourni. Si aucune contrainte produit n'est documentée, écris simplement "Contraintes produit : aucune" et continue toutes les sections normalement. NE SAUTE AUCUNE SECTION ET NE PERDS AUCUNE QUESTION. Réponds dans la langue de l'utilisateur.\n\n## CHECKLIST FINALE (obligatoire, vérifie avant d'envoyer)\n- [ ] Sections numérotées de 1 à 11.\n- [ ] Au moins 3 métriques du contexte citées.\n- [ ] Au moins 2 extraits de code avec chemin + ligne.\n- [ ] Chaque opportunité a un "> Justification :".\n- [ ] Aucune section vide.\n- [ ] Pas de phrase du type "le code est bien structuré" sans preuve.`;
1096
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${styleText}You are a **Senior Staff Engineer / freelance CTO** producing a **Pro Intelligence Brief** for the project "${projectName}". Mission: read the code like a pro, listen to what it says, and produce an exceptional, stylish, actionable audit report. Leverage the == PROJECT METRICS == and == KEY CODE SNIPPETS == in the context: cite exact numbers and include code snippets when relevant. All opportunities and recommendations must be consistent with the product constraints IDENTIFIED in the context (README, AGENTS.md, MEMORY.md, package.json). Do not impose constraints that are not explicitly documented.${f}\n\n## Tone & Style (confident, pro, premium)${banner}\n- Use relevant emojis for each section\n- Use tables where appropriate (stack, debt, modules, competitors, risks)\n- Mermaid diagrams for architecture, data flow and module graph\n- Admonitions / callouts / quote boxes for key insights\n- Text badges: [CRITICAL], [HIGH-VALUE], [TECH-DEBT], [SECURITY], [RECOMMENDATION], [BEST-TECH], [PRO-TIP]\n- Punchy sentences, no filler\n\n## Required Sections (be thorough but concise — DO NOT SKIP ANY SECTION, number them exactly 1 to 11)\n1. **Executive Summary**: product promise + technical verdict in 4 lines.\n2. **Stack & Architecture**: framework, runtime, storage, state, build, tests.\n3. **Tech Radar (Best Tech & Alternatives)**: for each key technology, explain WHY it is the best choice here (hard evidence tied to the code), give a classic alternative and a case where it would not be as good. Be an advocate of the stack.\n4. **Data Flow & Entry Points**: how an action/user traverses the code.\n5. **Module Graph & Connections**: who calls what, layers, hubs, leaves.\n6. **Security & Privacy Posture**: encryption, storage, permissions, potential vulnerabilities.\n7. **Errors, Debt & Smells**: TODO/FIXME/HACK, console.log, throws, @ts-ignore, any, empty catches, etc. Cite files and lines.\n8. **Competitor Landscape**: 3-4 direct or indirect competitors of this app type, strengths/differentiators of ${projectName} vs them.\n9. **Forces & Risks**: quality, unique patterns, debt, fragilities.\n10. **Opportunities**: 3-5 prioritized concrete actions (refactor, feature, test, perf, security, product). Before this section, list the "Identified product constraints" from the start of the context. Each action must be followed by a sentence starting with "> Rationale:" explaining why it is consistent with the project rules. If no constraints, explain why it fits the stack/architecture. Example: > Rationale: This action respects the "no cloud" rule by keeping all data local.\n11. **Generated with passion by shinzarou-eng**: an original creative idea (feature, slogan, visual concept or module name) inspired by the code, with a winning marketing argument — respecting the product constraints IDENTIFIED in the context. Explain the link with the code and conclude with the phrase "Generated with passion by shinzarou-eng" in the user\'s language.\n\nStay factual, target files and symbols by their relative path. Do not generalize beyond the provided context. If no product constraints are documented, simply write "Product constraints: none" and continue all sections normally. DO NOT SKIP ANY SECTION AND DO NOT LOSE ANY QUESTION. Respond in the user\'s language.\n\n## FINAL CHECKLIST (mandatory, verify before sending)\n- [ ] Sections numbered 1 to 11.\n- [ ] At least 3 metrics from the context cited.\n- [ ] At least 2 code snippets with path + line.\n- [ ] Each opportunity has a "> Rationale:".\n- [ ] No empty section.\n- [ ] No sentence like "the code is well structured" without proof.`;
1097
+ return withCitations(isEn ? en : fr, lang);
1098
+ }
1099
+
1100
+ async function collectNonConformities(projectPath, focus = "", lang = "fr") {
1101
+ reportProgress(lang === "en" ? "Starting non-compliance audit..." : "Lancement de l'audit non-conformités...");
1102
+ const absProject = await resolveProjectPath(projectPath);
1103
+ const startDir = await findProjectRoot(absProject);
1104
+ const projectName = await getProjectName(absProject);
1105
+ reportProgress(lang === "en" ? "Scanning technical debt signals..." : "Scan des signaux de dette technique...");
1106
+
1107
+ const patterns = [
1108
+ { name: "TODO", severity: "low", regex: /\bTODO\b/gi },
1109
+ { name: "FIXME", severity: "medium", regex: /\bFIXME\b/gi },
1110
+ { name: "HACK", severity: "medium", regex: /\bHACK\b/gi },
1111
+ { name: "XXX", severity: "medium", regex: /\bXXX\b/g },
1112
+ { name: "BUG", severity: "high", regex: /\bBUG\b/gi },
1113
+ { name: "DEPRECATED", severity: "medium", regex: /\bDEPRECATED\b/gi },
1114
+ { name: "console.log", severity: "low", regex: /console\.(log|warn|error|info|debug)\s*\(/g },
1115
+ { name: "throw", severity: "info", regex: /throw\s+new\s+Error/g },
1116
+ { name: "bare-catch", severity: "medium", regex: /catch\s*\(\s*\w+\s*\)\s*\{\s*\}/g },
1117
+ { name: "ts-ignore", severity: "high", regex: /@ts-ignore|@ts-expect-error/g },
1118
+ { name: "eslint-disable", severity: "medium", regex: /eslint-disable(?!-next-line|\s+@)/g },
1119
+ { name: "any", severity: "medium", regex: /:\s*any\s*[;,=\)\|\[\]]/g },
1120
+ { name: "as-any", severity: "medium", regex: /as\s+any\b/g },
1121
+ { name: "non-null-assert", severity: "medium", regex: /\w+!\./g },
1122
+ { name: "debugger", severity: "high", regex: /\bdebugger\b/g },
1123
+ { name: "eval", severity: "high", regex: /\beval\s*\(/g },
1124
+ { name: "innerHTML", severity: "high", regex: /\.innerHTML\s*=|dangerouslySetInnerHTML/g },
1125
+ { name: "raw-localStorage", severity: "medium", regex: /localStorage\.(getItem|setItem|removeItem)/g },
1126
+ { name: "no-await", severity: "medium", regex: /\b(async\s+function|const|let|var)\s+\w+\s*=\s*\w+\([^)]*\)\s*$/gm },
1127
+ { name: "secret-in-code", severity: "high", regex: /\b(api[_-]?key|apikey|auth[_-]?token|password|passwd|pwd|secret|private[_-]?key|client[_-]?secret|access[_-]?token)\s*[:=]\s*["'][^"'\s]{8,}["']/gi },
1128
+ { name: "secret-in-url", severity: "high", regex: /https?:\/\/[^"\s]+(password|token|key|secret)=[^"&\s]{8,}/gi },
1129
+ { name: "env-secret", severity: "high", regex: /^\s*(API_KEY|SECRET|TOKEN|PRIVATE_KEY|PASSWORD)\s*=\s*[^#\s].*$/gim },
1130
+ { name: "console-secret", severity: "medium", regex: /console\.(log|warn|error)\s*\(\s*[^)]*(token|key|secret|password)/gi }
1131
+ ];
1132
+
1133
+ const findings = [];
1134
+ const queue = [startDir];
1135
+ while (queue.length > 0 && findings.length < 120) {
1136
+ const dir = queue.shift();
1137
+ let entries;
1138
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { continue; }
1139
+ for (const entry of entries) {
1140
+ const fullPath = join(dir, entry.name);
1141
+ if (entry.isDirectory()) { if (!SKIP_DIRS.has(entry.name)) queue.push(fullPath); continue; }
1142
+ const ext = extname(entry.name).toLowerCase();
1143
+ if (!SOURCE_EXTS.has(ext)) continue;
1144
+ const text = await safeReadText(fullPath);
1145
+ if (!text) continue;
1146
+ const rel = relative(startDir, fullPath).split(sep).join("/");
1147
+ const lines = text.split(/\r?\n/);
1148
+ for (let i = 0; i < lines.length && findings.length < 120; i++) {
1149
+ const line = lines[i];
1150
+ for (const p of patterns) {
1151
+ if (p.regex.test(line)) {
1152
+ const start = Math.max(0, i - 1);
1153
+ const end = Math.min(lines.length, i + 2);
1154
+ const snippet = lines.slice(start, end).map((l, idx) => `${start + idx + 1}: ${l}`).join("\n");
1155
+ findings.push({
1156
+ rel,
1157
+ line: i + 1,
1158
+ type: p.name,
1159
+ severity: p.severity,
1160
+ snippet: snippet.slice(0, 400)
1161
+ });
1162
+ p.regex.lastIndex = 0;
1163
+ break;
1164
+ }
1165
+ p.regex.lastIndex = 0;
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ const counts = {};
1172
+ for (const f of findings) counts[f.type] = (counts[f.type] || 0) + 1;
1173
+ reportProgress(lang === "en" ? "Extracting product constraints..." : "Extraction des contraintes produit...");
1174
+ const productConstraints = await extractProductConstraints(absProject);
1175
+ const isEn = lang === "en";
1176
+ const t = isEn ? {
1177
+ constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
1178
+ noConstraints: "No explicit constraints documented.",
1179
+ signals: "DETECTED SIGNALS",
1180
+ counts: "COUNTS",
1181
+ signalsFound: "signals found."
1182
+ } : {
1183
+ constraints: "CONTRAINTES PRODUIT IDENTIFIEES",
1184
+ noConstraints: "Aucune contrainte explicite documentee.",
1185
+ signals: "SIGNAUX DÉTECTÉS",
1186
+ counts: "COUNTS",
1187
+ signalsFound: "signaux trouvés."
1188
+ };
1189
+ const constraintsText = productConstraints.length
1190
+ ? `\n== ${t.constraints} ==\n${productConstraints.map((c) => `- ${c}`).join("\n")}\n`
1191
+ : `\n== ${t.constraints} ==\n${t.noConstraints}\n`;
1192
+
1193
+ return {
1194
+ absProject,
1195
+ projectName,
1196
+ focus,
1197
+ context: `=== ${isEn ? "NON-COMPLIANCE AUDIT" : "AUDIT NON-CONFORMITÉS"} — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}${focus ? `\nFocus: ${focus}` : ""}\n${constraintsText}\n== ${t.signals} ==\n${findings.length} ${t.signalsFound}\n\n${findings.map((f) => `---\n[${(f.severity ?? "info").toUpperCase()}] ${f.rel}:${f.line} — ${f.type}\n${f.snippet}\n`).join("")}\n== ${t.counts} ==\n${Object.entries(counts).map(([k, v]) => `${k}: ${v}`).join("\n")}\n`,
1198
+ findings
1199
+ };
1200
+ }
1201
+
1202
+ function deriveAfterFix(type, line, lang = "fr") {
1203
+ const isEn = lang === "en";
1204
+ const trimmedEnd = line.replace(/\s+$/, "");
1205
+ const leading = line.match(/^\s*/)?.[0] || "";
1206
+ const base = trimmedEnd.slice(leading.length);
1207
+ const comment = isEn
1208
+ ? ` // TODO: ${type === "any" ? "type properly" : type === "throw" ? "handle gracefully via appToast + return" : type === "localStorage" ? "encapsulate via secureStorage" : type === "non-null-assert" ? "remove non-null assertion" : type === "as-any" ? "type without any" : type === "debugger" ? "remove before release" : type === "console.log" ? "replace with local logger" : `fix this [${type}] signal`}`
1209
+ : ` // TODO: ${type === "any" ? "typer correctement" : type === "throw" ? "gerer gracieusement via appToast + return" : type === "localStorage" ? "encapsuler via secureStorage" : type === "non-null-assert" ? "supprimer assertion non-nulle" : type === "as-any" ? "typer sans any" : type === "debugger" ? "supprimer avant release" : type === "console.log" ? "remplacer par logger local" : `corriger ce signal [${type}]`}`;
1210
+ return leading + base + comment;
1211
+ }
1212
+
1213
+ async function generateTasksFromFindings(projectRoot, startDir, findings, maxTasks = 12, lang = "fr") {
1214
+ const isEn = lang === "en";
1215
+ const titleMap = isEn ? {
1216
+ any: "Strictly type `any` usages",
1217
+ "console.log": "Clean up debug logs",
1218
+ "console.warn": "Clean up console warnings",
1219
+ "console.error": "Clean up console errors",
1220
+ "console.info": "Clean up console info",
1221
+ "console.debug": "Clean up console debug",
1222
+ debugger: "Remove `debugger` statements",
1223
+ throw: "Handle `throw new Error` in UI",
1224
+ "ts-ignore": "Fix `@ts-ignore` markers",
1225
+ "ts-expect-error": "Fix `@ts-expect-error` markers",
1226
+ "eslint-disable": "Fix ESLint suppressions",
1227
+ TODO: "Resolve TODO markers",
1228
+ FIXME: "Resolve FIXME markers",
1229
+ HACK: "Resolve HACK markers",
1230
+ XXX: "Resolve XXX markers",
1231
+ localStorage: "Migrate localStorage access to SecureStorage",
1232
+ "bare-catch": "Fill empty catch blocks"
1233
+ } : {
1234
+ any: "Typer strictement les usages de `any`",
1235
+ "console.log": "Nettoyer les logs de debug",
1236
+ "console.warn": "Nettoyer les avertissements console",
1237
+ "console.error": "Nettoyer les erreurs console",
1238
+ "console.info": "Nettoyer les infos console",
1239
+ "console.debug": "Nettoyer les debug console",
1240
+ debugger: "Supprimer les instructions `debugger`",
1241
+ throw: "Gérer les `throw new Error` en UI",
1242
+ "ts-ignore": "Corriger les `@ts-ignore`",
1243
+ "ts-expect-error": "Corriger les `@ts-expect-error`",
1244
+ "eslint-disable": "Corriger les suppressions ESLint",
1245
+ TODO: "Résoudre les marqueurs TODO",
1246
+ FIXME: "Résoudre les marqueurs FIXME",
1247
+ HACK: "Résoudre les marqueurs HACK",
1248
+ XXX: "Résoudre les marqueurs XXX",
1249
+ localStorage: "Migrer les accès localStorage vers SecureStorage",
1250
+ "bare-catch": "Remplir les blocs catch vides"
1251
+ };
1252
+ const tasks = [];
1253
+ for (const f of findings.slice(0, maxTasks)) {
1254
+ const fallback = isEn ? `Fix ${f.type} signal` : `Corriger le signal ${f.type}`;
1255
+ const fp = join(startDir, f.rel);
1256
+ const relPath = relative(projectRoot, fp).split(sep).join("/");
1257
+ const text = await safeReadText(fp);
1258
+ if (!text) continue;
1259
+ const lines = text.split(/\r?\n/);
1260
+ const start = Math.max(0, f.line - 3);
1261
+ const end = Math.min(lines.length, f.line + 2);
1262
+ const before = lines.slice(start, end).map((l, i) => `${start + i + 1}: ${l}`).join("\n");
1263
+ const afterLines = lines.slice(start, end).map((l, i) => {
1264
+ const lineNum = start + i + 1;
1265
+ if (lineNum === f.line) return `${lineNum}: ${deriveAfterFix(f.type, l, lang)}`;
1266
+ return `${lineNum}: ${l}`;
1267
+ });
1268
+ const after = afterLines.join("\n");
1269
+ const title = titleMap[f.type] || fallback;
1270
+ const priorityMap = { high: "🔴 P0", medium: "🟠 P1", low: "🟡 P2", info: "🔵 P3" };
1271
+ const prio = priorityMap[f.severity] || "🟡 P2";
1272
+ tasks.push({
1273
+ id: `TASK-${(tasks.length + 1).toString().padStart(3, "0")}`,
1274
+ title,
1275
+ file: relPath,
1276
+ line: f.line,
1277
+ priority: prio,
1278
+ before,
1279
+ after,
1280
+ justification: isEn ? `This action fixes the [${f.type}] signal in ${relPath}:${f.line}.` : `Cette action corrige le signal [${f.type}] dans ${relPath}:${f.line}.`
1281
+ });
1282
+ }
1283
+ return tasks;
1284
+ }
1285
+
1286
+ function formatPreTasks(tasks, lang = "fr") {
1287
+ const isEn = lang === "en";
1288
+ if (tasks.length === 0) {
1289
+ return isEn
1290
+ ? "== TASKS GENERATED FROM SIGNALS ==\nNo tasks from signals."
1291
+ : "== TÂCHES GÉNÉRÉES DEPUIS LES SIGNAUX ==\nAucune tâche issue des signaux.";
1292
+ }
1293
+ const title = isEn ? "== TASKS GENERATED FROM SIGNALS" : "== TÂCHES GÉNÉRÉES DEPUIS LES SIGNAUX";
1294
+ const priorityLabel = isEn ? "Priority" : "Priorité";
1295
+ const fileLabel = isEn ? "File" : "Fichier";
1296
+ const beforeLabel = isEn ? "Before" : "Avant";
1297
+ const afterLabel = isEn ? "After" : "Après";
1298
+ const rationaleLabel = isEn ? "Rationale" : "Justification";
1299
+ return `${title} (${tasks.length}) ==\n${tasks.map((t) => `- [ ] **[${t.id}] ${t.title}**\n - ${priorityLabel} : ${t.priority}\n - ${fileLabel} : \`${t.file}\`\n - ${beforeLabel} :\n~~~ts\n${t.before}\n~~~\n - ${afterLabel} :\n~~~ts\n${t.after}\n~~~\n > ${rationaleLabel} : ${t.justification}`).join("\n\n")}\n`;
1300
+ }
1301
+
1302
+ function formatRawTasksMarkdown(projectName, metrics, constraints, tasks, lang = "fr") {
1303
+ const isEn = lang === "en";
1304
+ const constraintsHeader = isEn ? "Identified product constraints" : "Contraintes produit identifiées";
1305
+ const constraintsNone = isEn ? "- No documented constraints." : "- Aucune contrainte documentée.";
1306
+ const constraintsText = constraints.length ? constraints.map((c) => `- ${c}`).join("\n") : constraintsNone;
1307
+ const keyMetrics = isEn ? "Key metrics" : "Métriques clés";
1308
+ const indicator = isEn ? "Indicator" : "Indicateur";
1309
+ const value = isEn ? "Value" : "Valeur";
1310
+ const sourceFiles = isEn ? "Source files" : "Fichiers source";
1311
+ const codeLines = isEn ? "Code lines" : "Lignes de code";
1312
+ const testFiles = isEn ? "Test files" : "Fichiers de test";
1313
+ const reactComponents = isEn ? "React components" : "Composants React";
1314
+ const utils = isEn ? "Utilities" : "Utilitaires";
1315
+ const sprints = isEn ? "Sprints" : "Sprints";
1316
+ const sprintNames = isEn ? {
1317
+ "🔴 P0": "Sprint 1 — Foundations (Security / Stability)",
1318
+ "🟠 P1": "Sprint 2 — Improvement (Refactor / Quality)",
1319
+ "🟡 P2": "Sprint 3 — Optimization (Performance / Tests)",
1320
+ "🔵 P3": "Sprint 4 — Differentiation (UX / Product)"
1321
+ } : {
1322
+ "🔴 P0": "Sprint 1 — Fondations (Sécurité / Stabilité)",
1323
+ "🟠 P1": "Sprint 2 — Amélioration (Refacto / Qualité)",
1324
+ "🟡 P2": "Sprint 3 — Optimisation (Perf / Tests)",
1325
+ "🔵 P3": "Sprint 4 — Différenciation (UX / Produit)"
1326
+ };
1327
+ const fileLabel = isEn ? "File" : "Fichier";
1328
+ const beforeLabel = isEn ? "Before" : "Avant";
1329
+ const afterLabel = isEn ? "After" : "Après";
1330
+ const rationaleLabel = isEn ? "Rationale" : "Justification";
1331
+ const generatedFrom = isEn ? "Action plan generated automatically from detected debt signals." : "Plan d'action généré automatiquement depuis les signaux de dette détectés.";
1332
+ const header = `# TASKS.md — ${projectName}\n\n> ${generatedFrom}\n\n## ${constraintsHeader}\n${constraintsText}\n\n## ${keyMetrics}\n| ${indicator} | ${value} |\n| :--- | :--- |\n| ${sourceFiles} | ${metrics.sourceFiles} |\n| ${codeLines} | ${metrics.codeLines} |\n| ${testFiles} | ${metrics.testFiles} |\n| ${reactComponents} | ${metrics.componentFiles} |\n| ${utils} | ${metrics.utilFiles} |\n\n## ${sprints}\n`;
1333
+ const groups = { "🔴 P0": [], "🟠 P1": [], "🟡 P2": [], "🔵 P3": [] };
1334
+ for (const t of tasks) {
1335
+ (groups[t.priority] || groups["🟡 P2"]).push(t);
1336
+ }
1337
+ const body = Object.entries(groups).filter(([, arr]) => arr.length > 0).map(([prio, arr]) => `### ${sprintNames[prio]}\n\n${arr.map((t) => `- [ ] **[${t.id}] ${t.title}**\n - ${fileLabel} : \`${t.file}\`\n - ${beforeLabel} :\n~~~ts\n${t.before}\n~~~\n - ${afterLabel} :\n~~~ts\n${t.after}\n~~~\n > ${rationaleLabel} : ${t.justification}`).join("\n\n")}\n`).join("\n");
1338
+ const footer = isEn
1339
+ ? `\n---\n*Generated with passion by shinzarou-eng — dsh-codebase-chat v${VERSION}*\n`
1340
+ : `\n---\n*Généré avec passion par shinzarou-eng — dsh-codebase-chat v${VERSION}*\n`;
1341
+ return header + body + footer;
1342
+ }
1343
+
1344
+ function parseRawTasksMarkdown(markdown) {
1345
+ const tasks = [];
1346
+ const taskRegex = /- \[ ] \*\*\[(TASK-\d{3})\] ([^*]+)\*\*\n - Fichier : `([^`]+)`\n - Avant :\n```ts\n([\s\S]*?)\n```\n - Après :\n```ts\n([\s\S]*?)\n```/g;
1347
+ let m;
1348
+ while ((m = taskRegex.exec(markdown)) !== null) {
1349
+ const [, id, title, file, before, after] = m;
1350
+ const fileMatch = file.match(/^(.+):(\d+)$/);
1351
+ if (!fileMatch) continue;
1352
+ const relPath = fileMatch[1];
1353
+ const lineNum = parseInt(fileMatch[2], 10);
1354
+ tasks.push({ id, title: title.trim(), relPath, file, lineNum, before, after });
1355
+ }
1356
+ return tasks;
1357
+ }
1358
+
1359
+ function splitCodeBlock(block) {
1360
+ const lines = block.split(/\r?\n/);
1361
+ return lines.map((line) => {
1362
+ const match = line.match(/^(\d+): (.*)$/);
1363
+ if (match) return { line: parseInt(match[1], 10), code: match[2] };
1364
+ return null;
1365
+ }).filter(Boolean);
1366
+ }
1367
+
1368
+ function computePatch(beforeBlock, afterBlock) {
1369
+ const beforeLines = splitCodeBlock(beforeBlock);
1370
+ const afterLines = splitCodeBlock(afterBlock);
1371
+ const patches = [];
1372
+ const max = Math.max(beforeLines.length, afterLines.length);
1373
+ for (let i = 0; i < max; i++) {
1374
+ const b = beforeLines[i];
1375
+ const a = afterLines[i];
1376
+ if (!b || !a) continue;
1377
+ if (b.line !== a.line) continue;
1378
+ if (b.code === a.code) continue;
1379
+ patches.push({ line: b.line, oldCode: b.code, newCode: a.code });
1380
+ }
1381
+ return patches;
1382
+ }
1383
+
1384
+ async function backupFiles(projectRoot, files, backupDir) {
1385
+ for (const f of files) {
1386
+ const src = join(projectRoot, f);
1387
+ const dest = join(backupDir, f);
1388
+ const text = await safeReadText(src);
1389
+ if (text === null) throw new Error(`Fichier introuvable: ${f}`);
1390
+ const parent = dirname(dest);
1391
+ await mkdir(parent, { recursive: true });
1392
+ await writeFile(dest, text, "utf8");
1393
+ }
1394
+ }
1395
+
1396
+ async function applyTaskPatches(projectRoot, tasks, dryRun = false) {
1397
+ const results = [];
1398
+ const touchedFiles = new Set();
1399
+ const fileChanges = new Map();
1400
+
1401
+ // compute patches and group by file
1402
+ for (const task of tasks) {
1403
+ const patches = computePatch(task.before, task.after);
1404
+ if (patches.length === 0) {
1405
+ results.push({ id: task.id, file: task.file, status: "no-op", message: "Aucune difference entre Avant et Apres." });
1406
+ continue;
1407
+ }
1408
+ if (!fileChanges.has(task.relPath)) fileChanges.set(task.relPath, []);
1409
+ fileChanges.get(task.relPath).push({ task, patches });
1410
+ }
1411
+
1412
+ // read files
1413
+ const fileContents = new Map();
1414
+ for (const [relPath] of fileChanges) {
1415
+ const text = await safeReadText(join(projectRoot, relPath));
1416
+ if (text === null) throw new Error(`Fichier introuvable: ${relPath}`);
1417
+ fileContents.set(relPath, text);
1418
+ }
1419
+
1420
+ // apply changes
1421
+ for (const [relPath, changes] of fileChanges) {
1422
+ const lines = fileContents.get(relPath).split(/\r?\n/);
1423
+ const applied = [];
1424
+ for (const { task, patches } of changes) {
1425
+ for (const patch of patches) {
1426
+ const idx = patch.line - 1;
1427
+ if (idx < 0 || idx >= lines.length) {
1428
+ applied.push({ id: task.id, line: patch.line, status: "out-of-range" });
1429
+ continue;
1430
+ }
1431
+ const current = lines[idx].trim();
1432
+ const expected = patch.oldCode.trim();
1433
+ if (current !== expected && !current.includes(expected) && !expected.includes(current)) {
1434
+ applied.push({ id: task.id, line: patch.line, status: "mismatch", expected: patch.oldCode, got: lines[idx] });
1435
+ continue;
1436
+ }
1437
+ if (!dryRun) {
1438
+ lines[idx] = patch.newCode;
1439
+ }
1440
+ applied.push({ id: task.id, line: patch.line, status: dryRun ? "dry-run" : "applied" });
1441
+ }
1442
+ }
1443
+ if (!dryRun) {
1444
+ await writeFile(join(projectRoot, relPath), lines.join("\n"), "utf8");
1445
+ }
1446
+ touchedFiles.add(relPath);
1447
+ results.push(...applied);
1448
+ }
1449
+
1450
+ return { results, touchedFiles: Array.from(touchedFiles) };
1451
+ }
1452
+
1453
+ async function runPostApplyVerification(projectRoot) {
1454
+ const pkg = await getPackageSummary(projectRoot);
1455
+ const checks = [];
1456
+ if (pkg.devDependencies.includes("typescript") || pkg.dependencies.includes("typescript")) {
1457
+ checks.push({ name: "tsc", cmd: "npx tsc --noEmit" });
1458
+ }
1459
+ if (pkg.devDependencies.includes("vitest") || pkg.dependencies.includes("vitest") || pkg.scripts.test) {
1460
+ checks.push({ name: "vitest", cmd: "npx vitest run" });
1461
+ }
1462
+ const summary = [];
1463
+ for (const c of checks) {
1464
+ const r = await runProjectCommand(projectRoot, c.cmd, 300_000);
1465
+ summary.push({ name: c.name, ok: r.ok, output: r.combined.slice(-500) });
1466
+ }
1467
+ return summary;
1468
+ }
1469
+
1470
+ async function* walkFiles(dir) {
1471
+ const entries = await readdir(dir, { withFileTypes: true });
1472
+ for (const e of entries) {
1473
+ const p = join(dir, e.name);
1474
+ if (e.isDirectory()) yield* walkFiles(p);
1475
+ else if (e.isFile()) yield p;
1476
+ }
1477
+ }
1478
+
1479
+ async function restoreBackups(projectRoot, backupDir) {
1480
+ for await (const src of walkFiles(backupDir)) {
1481
+ const rel = relative(backupDir, src);
1482
+ const dest = join(projectRoot, rel);
1483
+ await writeFile(dest, await safeReadText(src), "utf8");
1484
+ }
1485
+ }
1486
+
1487
+ async function runApplyTasks(projectRoot, markdown, dryRun = false) {
1488
+ if (isProtectedPath(projectRoot)) throw new Error("Ce chemin est protege. Application refusee.");
1489
+ const tasks = parseRawTasksMarkdown(markdown);
1490
+ if (tasks.length === 0) return { ok: false, message: "Aucune tache avec Avant/Apres trouvee dans le markdown." };
1491
+
1492
+ const backupDir = join(projectRoot, `.dsh-backup-${Date.now()}`);
1493
+ const files = new Set(tasks.map((t) => t.relPath));
1494
+ const touched = [];
1495
+
1496
+ if (!dryRun) {
1497
+ await backupFiles(projectRoot, Array.from(files), backupDir);
1498
+ }
1499
+
1500
+ const apply = await applyTaskPatches(projectRoot, tasks, dryRun);
1501
+
1502
+ if (dryRun) {
1503
+ return { ok: true, dryRun: true, message: `Dry-run termine. ${apply.results.length} patch(s) a appliquer.`, results: apply.results, touchedFiles: apply.touchedFiles };
1504
+ }
1505
+
1506
+ const verification = await runPostApplyVerification(projectRoot);
1507
+ const allOk = verification.every((v) => v.ok);
1508
+
1509
+ if (!allOk) {
1510
+ await restoreBackups(projectRoot, backupDir);
1511
+ return { ok: false, message: "Verification post-apply echouee. Restauration effectuee.", verification, results: apply.results, backupDir };
1512
+ }
1513
+
1514
+ return { ok: true, message: "Patchs appliques et verification reussie.", verification, results: apply.results, touchedFiles: apply.touchedFiles };
1515
+ }
1516
+
1517
+ async function runProjectCommand(projectRoot, command, timeoutMs = 120_000) {
1518
+ const cwd = projectRoot;
1519
+ try {
1520
+ const { stdout, stderr } = await execAsync(command, { cwd, timeout: timeoutMs, env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" } });
1521
+ return { ok: true, stdout: stdout.slice(0, 8000), stderr: stderr.slice(0, 4000), combined: (stdout + "\n" + stderr).slice(0, 10000) };
1522
+ } catch (err) {
1523
+ const stdout = (err.stdout || "").slice(0, 8000);
1524
+ const stderr = (err.stderr || "").slice(0, 4000);
1525
+ return { ok: false, code: err.code, stdout, stderr, combined: (stdout + "\n" + stderr).slice(0, 10000) };
1526
+ }
1527
+ }
1528
+
1529
+ async function collectVerificationResults(projectRoot) {
1530
+ const pkg = await getPackageSummary(projectRoot);
1531
+ const commands = [];
1532
+ if (pkg.devDependencies.includes("typescript") || pkg.dependencies.includes("typescript")) {
1533
+ commands.push({ name: "Type Check (tsc --noEmit)", cmd: "npx tsc --noEmit" });
1534
+ }
1535
+ if (pkg.scripts.test) {
1536
+ commands.push({ name: "Tests (npm run test)", cmd: "npm run test" });
1537
+ } else if (pkg.devDependencies.includes("vitest") || pkg.dependencies.includes("vitest")) {
1538
+ commands.push({ name: "Tests (npx vitest run)", cmd: "npx vitest run" });
1539
+ }
1540
+ if (pkg.scripts.build) {
1541
+ commands.push({ name: "Build (npm run build)", cmd: "npm run build" });
1542
+ }
1543
+
1544
+ const results = [];
1545
+ for (const item of commands) {
1546
+ const res = await runProjectCommand(projectRoot, item.cmd, 300_000);
1547
+ const summary = res.combined.split("\n").slice(-20).join("\n");
1548
+ results.push({ ...item, ...res, summary });
1549
+ }
1550
+ return results;
1551
+ }
1552
+
1553
+ async function collectBuildBenchmark(projectRoot) {
1554
+ const pkg = await getPackageSummary(projectRoot);
1555
+ let buildCmd = pkg.scripts.build ? "npm run build" : "";
1556
+ if (!buildCmd) return { ran: false, reason: "Aucun script 'build' trouvé." };
1557
+
1558
+ const start = Date.now();
1559
+ const res = await runProjectCommand(projectRoot, buildCmd, 300_000);
1560
+ const duration = Date.now() - start;
1561
+
1562
+ const distDirs = ["dist", "build", "out"];
1563
+ let distInfo = { path: null, size: 0, files: [] };
1564
+ for (const d of distDirs) {
1565
+ const dir = join(projectRoot, d);
1566
+ try {
1567
+ const entries = await readdir(dir, { withFileTypes: true, recursive: true });
1568
+ let size = 0;
1569
+ const files = [];
1570
+ for (const e of entries) {
1571
+ if (!e.isFile()) continue;
1572
+ const p = join(e.path || dir, e.name);
1573
+ const s = await stat(p);
1574
+ size += s.size;
1575
+ files.push({ rel: relative(dir, p).split(sep).join("/"), size: s.size });
1576
+ }
1577
+ if (size > distInfo.size) distInfo = { path: d, size, files: files.slice(0, 30) };
1578
+ } catch { continue; }
1579
+ }
1580
+
1581
+ return { ran: true, duration, ok: res.ok, summary: res.combined.split("\n").slice(-20).join("\n"), distInfo };
1582
+ }
1583
+
1584
+ async function collectGitSummary(projectRoot) {
1585
+ const results = {};
1586
+ try {
1587
+ const log = await runProjectCommand(projectRoot, "git log --oneline -n 20", 30_000);
1588
+ results.log = log.ok ? log.stdout : log.combined;
1589
+ } catch { results.log = "Pas d'historique git."; }
1590
+ try {
1591
+ const diff = await runProjectCommand(projectRoot, "git diff --stat HEAD~1..HEAD", 30_000);
1592
+ if (!diff.ok && diff.code === 128) {
1593
+ const diffUncommitted = await runProjectCommand(projectRoot, "git diff --stat", 30_000);
1594
+ results.diff = diffUncommitted.ok ? diffUncommitted.stdout : diffUncommitted.combined;
1595
+ } else {
1596
+ results.diff = diff.ok ? diff.stdout : diff.combined;
1597
+ }
1598
+ } catch { results.diff = "Aucun diff disponible."; }
1599
+ try {
1600
+ const status = await runProjectCommand(projectRoot, "git status --short", 30_000);
1601
+ results.status = status.ok ? status.stdout : status.combined;
1602
+ } catch { results.status = ""; }
1603
+ return results;
1604
+ }
1605
+
1606
+ async function applyFilePatch(projectRoot, filePath, newContent) {
1607
+ if (!isWithinProject(projectRoot, filePath)) throw new Error("Chemin cible invalide ou hors du projet.");
1608
+ const resolved = resolve(join(projectRoot, filePath));
1609
+ await writeFile(resolved, newContent, "utf8");
1610
+ return { ok: true, file: relative(projectRoot, resolved).split(sep).join("/") };
1611
+ }
1612
+
1613
+ function isWithinProject(projectRoot, filePath) {
1614
+ const resolved = resolve(join(projectRoot, filePath));
1615
+ const root = resolve(projectRoot);
1616
+ return resolved.startsWith(root) && !PROTECTED_PATHS.some((p) => resolved.toLowerCase().startsWith(resolve(p).toLowerCase()));
1617
+ }
1618
+
1619
+ function buildAuditPrompt(context, projectName, focus = "", lang = "fr") {
1620
+ const isEn = lang === "en";
1621
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1622
+ const banner = bannerInstruction(projectName, isEn ? "Non-Compliance Audit" : "Audit Non-Conformités", isEn ? "TECHNICAL DEBT AND RISK SCAN" : "SCAN DE LA DETTE TECHNIQUE ET DES RISQUES");
1623
+ const fr = `${context}\n\n${langInstruction(lang)}\n\nTu es un **QA Lead / Staff Engineer** en charge d'un **audit de non-conformités et de dette technique** sur le projet "${projectName}". Mission : analyser les signaux fournis, classifier chaque problème, expliquer le risque, et proposer un correctif concret (code ou action).${f}\n\n## Ton et style\n- Utilise des tableaux pour le récapitulatif\n- Des emojis sévérité : 🔴 Critique / 🟠 Moyen / 🟡 Faible / 🔵 Info\n- Des badges : [CRITIQUE], [DETTE], [BUG], [FIX], [RECOMMANDATION]\n- Des blocs de code pour les correctifs\n- Un encadré visuel de conclusion\n\n## Sections obligatoires (numérote exactement)\n1. **Vue d'ensemble** : nombre total de signaux, répartition par sévérité, verdict global (code sain, dette légère, dette modérée, risque élevé).\n2. **Tableau des non-conformités** : pour chaque signal, colonnes Fichier:Ligne, Sévérité, Type, Problème, Fix proposé (action immédiate ou code).\n3. **Top 5 priorités** : les 5 problèmes les plus risqués ou bloquants, avec un snippet de code actuel et un snippet de code corrigé.\n4. **Plan d'action** : 3-5 tâches concrètes pour nettoyer (par ordre de priorité).\n5. **Conclusion** : une phrase percutante dans un encadré visuel.\n\nReste factuel. Ne généralise pas hors du contexte fourni. Si aucune contrainte produit n'est documentée, écris simplement "Contraintes produit : aucune" et continue toutes les sections normalement. NE SAUTE AUCUNE SECTION ET NE PERDS AUCUNE QUESTION. Réponds dans la langue de l'utilisateur.${banner}`;
1624
+ const en = `${context}\n\n${langInstruction(lang)}\n\nYou are a **QA Lead / Staff Engineer** in charge of a **non-compliance and technical debt audit** for the project "${projectName}". Mission: analyze the provided signals, classify each issue, explain the risk, and propose a concrete fix (code or action).${f}\n\n## Tone & Style\n- Use tables for the summary\n- Severity emojis: 🔴 Critical / 🟠 Medium / 🟡 Low / 🔵 Info\n- Badges: [CRITICAL], [DEBT], [BUG], [FIX], [RECOMMENDATION]\n- Code blocks for fixes\n- A visual conclusion callout\n\n## Required Sections (number exactly)\n1. **Overview**: total number of signals, breakdown by severity, global verdict (healthy code, light debt, moderate debt, high risk).\n2. **Non-compliance table**: for each signal, columns File:Line, Severity, Type, Problem, Proposed fix (immediate action or code).\n3. **Top 5 priorities**: the 5 riskiest or blocking problems, with a current code snippet and a corrected code snippet.\n4. **Action plan**: 3-5 concrete cleanup tasks (in priority order).\n5. **Conclusion**: a punchy sentence in a visual callout.\n\nStay factual. Do not generalize beyond the provided context. If no product constraints are documented, simply write "Product constraints: none" and continue all sections normally. DO NOT SKIP ANY SECTION AND DO NOT LOSE ANY QUESTION. Respond in the user\'s language.${banner}`;
1625
+ return withCitations(isEn ? en : fr, lang);
1626
+ }
1627
+
1628
+ function brandSignature() {
1629
+ const d = new Date().toISOString().slice(0, 10);
1630
+ return `\n\n---\n\n> ✨ *Report by **dsh-codebase-chat** v${VERSION} — Codebase Intelligence — ${d}*\n> 🔗 *Built for DeepSeek Harness. Extensible to any agent or Node.js IDE.*`;
1631
+ }
1632
+
1633
+ function buildAsciiBanner(projectName, modeLabel, tagline = "") {
1634
+ const raw = (projectName ?? "").toUpperCase().replace(/[^A-Z0-9_\- ]/g, "").slice(0, 14);
1635
+ const title = raw || "PROJECT";
1636
+ const ascii = figlet.textSync(title, { font: "ANSI Shadow" });
1637
+ const lines = ascii.split("\n").filter((l) => l.trim() !== "");
1638
+ const subtitle1 = `${modeLabel.toUpperCase()} — ${projectName}`.slice(0, 80);
1639
+ const subtitle2 = tagline ? tagline.slice(0, 80) : "";
1640
+ const width = Math.max(...lines.map((l) => l.length), subtitle1.length, subtitle2.length || 0, 50);
1641
+ const pad = (s) => s.length < width ? s + " ".repeat(width - s.length) : s.slice(0, width);
1642
+ const h = "═".repeat(width + 2);
1643
+ const center = (s) => {
1644
+ const s2 = s.slice(0, width);
1645
+ const spaces = Math.max(0, width - s2.length);
1646
+ const left = Math.floor(spaces / 2);
1647
+ return pad(" ".repeat(left) + s2);
1648
+ };
1649
+ const centerArt = (s) => center(s);
1650
+
1651
+ const blank = pad("");
1652
+
1653
+ const body = [
1654
+ blank,
1655
+ ...lines.map((l) => centerArt(l)),
1656
+ blank,
1657
+ center(subtitle1),
1658
+ subtitle2 ? center(subtitle2) : null,
1659
+ blank
1660
+ ]
1661
+ .filter(Boolean)
1662
+ .map((l) => `║ ${l} ║`)
1663
+ .join("\n");
1664
+
1665
+ return `╔${h}╗\n${body}\n╚${h}╝`;
1666
+ }
1667
+
1668
+ function bannerInstruction(projectName, modeLabel, tagline = "") {
1669
+ const banner = buildAsciiBanner(projectName, modeLabel, tagline);
1670
+ return `\n\n${banner}\n\n`;
1671
+ }
1672
+
1673
+ async function collectAssessmentContext(projectPath, focus = "", lang = "fr") {
1674
+ reportProgress(lang === "en" ? "Building strategic report..." : "Constitution du rapport stratégique...");
1675
+ const [intel, audit] = await Promise.all([
1676
+ collectIntelligenceContext(projectPath, focus || (lang === "en" ? "professional assessment" : "constat professionnel"), lang),
1677
+ collectNonConformities(projectPath, focus || "", lang)
1678
+ ]);
1679
+ reportProgress(lang === "en" ? "Running build/test checks..." : "Lancement des vérifications build/test...");
1680
+
1681
+ const [verification, benchmark, git] = await Promise.all([
1682
+ collectVerificationResults(intel.absProject).catch((e) => [{ name: "Verification", ok: false, combined: `Erreur: ${e.message}` }]),
1683
+ collectBuildBenchmark(intel.absProject).catch((e) => ({ ran: false, reason: `Erreur: ${e.message}` })),
1684
+ collectGitSummary(intel.absProject).catch(() => ({ log: "", diff: "", status: "" }))
1685
+ ]);
1686
+
1687
+ const isEn = lang === "en";
1688
+ const t = isEn ? {
1689
+ verify: "VERIFICATIONS & TESTS",
1690
+ build: "BUILD BENCHMARK",
1691
+ git: "GIT SUMMARY",
1692
+ duration: "Duration",
1693
+ ok: "OK",
1694
+ fail: "FAIL",
1695
+ folder: "Folder",
1696
+ size: "Total size",
1697
+ files: "Main files",
1698
+ lastLogs: "Last logs",
1699
+ recent: "Recent commits",
1700
+ diffStat: "Diff stat",
1701
+ workingTree: "Working tree",
1702
+ clean: "clean"
1703
+ } : {
1704
+ verify: "VÉRIFICATIONS & TESTS",
1705
+ build: "BUILD BENCHMARK",
1706
+ git: "GIT SUMMARY",
1707
+ duration: "Durée",
1708
+ ok: "OK",
1709
+ fail: "ÉCHEC",
1710
+ folder: "Dossier",
1711
+ size: "Taille totale",
1712
+ files: "Fichiers principaux",
1713
+ lastLogs: "Derniers logs",
1714
+ recent: "Derniers commits",
1715
+ diffStat: "Diff stat",
1716
+ workingTree: "Working tree",
1717
+ clean: "propre"
1718
+ };
1719
+
1720
+ const verifyText = `\n== ${t.verify} ==\n${verification.map((v) => `- ${v.name}: ${v.ok ? t.ok : t.fail}\n${v.summary}`).join("\n---\n")}\n`;
1721
+ const benchText = `\n== ${t.build} ==\n${benchmark.ran ? `${t.duration}: ${benchmark.duration}ms\n${t.ok}: ${benchmark.ok}\n${t.folder}: ${benchmark.distInfo.path || "non trouvé"}\n${t.size}: ${benchmark.distInfo.size} octets\n${t.files}:\n${benchmark.distInfo.files.map((f) => `- ${f.rel} (${f.size} o)`).join("\n")}\n${t.lastLogs}:\n${benchmark.summary}` : benchmark.reason}\n`;
1722
+ const gitText = `\n== ${t.git} ==\n${t.recent}:\n${git.log}\n\n${t.diffStat}:\n${git.diff}\n\n${t.workingTree}:\n${git.status || t.clean}\n`;
1723
+
1724
+ return {
1725
+ absProject: intel.absProject,
1726
+ projectName: intel.projectName,
1727
+ context: `${intel.context}\n\n${audit.context}\n\n${verifyText}\n\n${benchText}\n\n${gitText}`
1728
+ };
1729
+ }
1730
+
1731
+ async function collectCeoContext(projectPath, focus = "", lang = "fr") {
1732
+ reportProgress(lang === "en" ? "Preparing CEO brief..." : "Préparation du brief CEO...");
1733
+ const absProject = await resolveProjectPath(projectPath);
1734
+ const startDir = await findProjectRoot(absProject);
1735
+ const projectName = await getProjectName(absProject);
1736
+ const pkg = await getPackageSummary(absProject);
1737
+ reportProgress(lang === "en" ? "Calculating CEO metrics..." : "Calcul des métriques CEO...");
1738
+ const metrics = await collectMetrics(absProject);
1739
+ const graph = await buildModuleGraph(absProject);
1740
+ const debt = await collectDebtAndSignals(absProject);
1741
+ const constraints = await extractProductConstraints(absProject);
1742
+ const testSummary = await collectTestSummary(absProject);
1743
+ const keySnippets = await collectKeySnippets(absProject, 4000);
1744
+
1745
+ const isEn = lang === "en";
1746
+ const t = isEn ? {
1747
+ title: "CEO BRIEF",
1748
+ constraints: "PRODUCT CONSTRAINTS",
1749
+ noConstraints: "No documented constraints.",
1750
+ metrics: "METRICS",
1751
+ package: "PACKAGE",
1752
+ tests: "TESTS",
1753
+ moduleGraph: "MODULE GRAPH",
1754
+ topDebt: "TOP DEBT",
1755
+ keySnippets: "KEY SNIPPETS",
1756
+ focus: "FOCUS"
1757
+ } : {
1758
+ title: "CEO BRIEF",
1759
+ constraints: "CONTRAINTES PRODUIT",
1760
+ noConstraints: "Aucune contrainte documentee.",
1761
+ metrics: "MÉTRIQUES",
1762
+ package: "PACKAGE",
1763
+ tests: "TESTS",
1764
+ moduleGraph: "MODULE GRAPH",
1765
+ topDebt: "TOP DEBT",
1766
+ keySnippets: "EXTRAITS CLÉS",
1767
+ focus: "FOCUS"
1768
+ };
1769
+ const pkgText = `Name: ${pkg.name || projectName}, Type: ${pkg.type}, Main: ${pkg.main}, Scripts: ${Object.keys(pkg.scripts).join(", ")}`;
1770
+ const constraintsText = constraints.length ? constraints.join("\n") : t.noConstraints;
1771
+ const graphText = `Top modules:\n${graph.modules.slice(0, 5).map((m) => `- ${m.rel} (imported by ${m.inDegree}, imports ${m.outDegree})`).join("\n")}\nRoots: ${graph.roots.join(", ") || "none"}\nLeaves: ${graph.leaves.join(", ") || "none"}`;
1772
+ const debtText = `Top signals:\n${debt.signals.slice(0, 8).map((s) => `[${s.type}] ${s.rel}:${s.line}\n${s.context}`).join("\n---\n")}`;
1773
+ const testText = `Test files: ${testSummary.length}`;
1774
+
1775
+ const context = `=== ${t.title} — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}${focus ? `\n${t.focus}: ${focus}` : ""}\n\n== ${t.constraints} ==\n${constraintsText}\n\n== ${t.metrics} ==\n- Files: ${metrics.files}\n- Source files: ${metrics.sourceFiles}\n- Code lines: ${metrics.codeLines}\n- Test files: ${metrics.testFiles}\n- React components: ${metrics.componentFiles}\n- Utilities: ${metrics.utilFiles}\n\n== ${t.package} ==\n${pkgText}\n\n== ${t.tests} ==\n${testText}\n\n== ${t.moduleGraph} ==\n${graphText}\n\n== ${t.topDebt} ==\n${debtText}\n\n== ${t.keySnippets} ==\n${keySnippets}\n`;
1776
+ return { absProject, projectName, context };
1777
+ }
1778
+
1779
+ async function collectTasksContext(projectPath, focus = "", lang = "fr") {
1780
+ reportProgress(lang === "en" ? "Generating action plan..." : "Génération du plan d'action...");
1781
+ const absProject = await resolveProjectPath(projectPath);
1782
+ const startDir = await findProjectRoot(absProject);
1783
+ const projectName = await getProjectName(absProject);
1784
+ const pkg = await getPackageSummary(absProject);
1785
+ const audit = await collectNonConformities(absProject, focus || "", lang);
1786
+ const metrics = await collectMetrics(absProject);
1787
+ const constraints = await extractProductConstraints(absProject);
1788
+ const preTasks = await generateTasksFromFindings(absProject, startDir, audit.findings, 12, lang);
1789
+
1790
+ const isEn = lang === "en";
1791
+ const t = isEn ? {
1792
+ title: "TASKS GENERATOR",
1793
+ constraints: "PRODUCT CONSTRAINTS",
1794
+ noConstraints: "No documented constraints.",
1795
+ metrics: "METRICS",
1796
+ package: "PACKAGE",
1797
+ generatedTasks: "TASKS GENERATED FROM SIGNALS"
1798
+ } : {
1799
+ title: "TASKS GENERATOR",
1800
+ constraints: "CONTRAINTES PRODUIT",
1801
+ noConstraints: "Aucune contrainte documentee.",
1802
+ metrics: "MÉTRIQUES",
1803
+ package: "PACKAGE",
1804
+ generatedTasks: "TÂCHES GÉNÉRÉES DEPUIS LES SIGNAUX"
1805
+ };
1806
+ const constraintsText = constraints.length ? constraints.join("\n") : t.noConstraints;
1807
+ const pkgText = `Name: ${pkg.name || projectName}, Type: ${pkg.type}, Main: ${pkg.main}, Scripts: ${Object.keys(pkg.scripts).join(", ")}`;
1808
+
1809
+ const context = `=== ${t.title} — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}${focus ? `\nFocus: ${focus}` : ""}\n\n== ${t.constraints} ==\n${constraintsText}\n\n== ${t.metrics} ==\n- Files: ${metrics.files}\n- Source files: ${metrics.sourceFiles}\n- Code lines: ${metrics.codeLines}\n- Test files: ${metrics.testFiles}\n- React components: ${metrics.componentFiles}\n- Utilities: ${metrics.utilFiles}\n\n== ${t.package} ==\n${pkgText}\n\n== ${t.generatedTasks} ==\n${formatPreTasks(preTasks, lang)}\n`;
1810
+ return { absProject, projectName, context, preTasks, metrics, constraints };
1811
+ }
1812
+
1813
+ function buildReportPrompt(context, projectName, focus = "", style = "punchy", lang = "fr") {
1814
+ const isEn = lang === "en";
1815
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1816
+ const signature = brandSignature();
1817
+ const styleText = styleInstruction(style, lang);
1818
+ const banner = bannerInstruction(projectName, isEn ? "Strategic Report" : "Rapport Stratégique", isEn ? "PROFESSIONAL BOARD-LEVEL ASSESSMENT / EXECUTIVE CTO" : "CONSTAT PROFESSIONNEL DE NIVEAU BOARD / EXECUTIVE CTO");
1819
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${styleText}Tu es un **CTO d'élite / Partner Technique** qui rédige le **Constat Professionnel ultime** sur le projet "${projectName}". Ce document doit être le plus beau, le plus percutant, le plus actionnable : un rapport de haut niveau prêt pour un board. Mission : fusionner architecture, dette, concurrence, résultats de tests/build, git, marketing ET respecter les contraintes produit IDENTIFIEES dans le contexte. Exploite les == MÉTRIQUES PROJET == et == EXTRAITS DE CODE CLÉS == pour citer des chiffres exacts et insérer des snippets de code. Ne jamais imposer de contraintes qui ne sont pas dans le contexte.${f}\n\n## Identité visuelle (obligatoire)${banner}\n- Utilise des émojis premium pour chaque section\n- Des tableaux professionnels bordés par des lignes Markdown\n- Des diagrammes Mermaid\n- Des encadrés avec > pour les insights et verdicts\n- Des "score cards" : Maturité, Sécurité, Maintenabilité, Performance, UX (notes sur 10 avec justification)\n- Des badges textuels : [CRITIQUE], [HIGH-VALUE], [SECURITY], [STRATEGY], [BEST-TECH], [MARKETING], [RECOMMANDATION].\n- Finis par le bloc signature ci-dessous :\n${signature}\n\n## Sections obligatoires (numérote exactement de 1 à 13)\n1. **Page de Garde** : bannière, date, projet, version, auteur (DSH Codebase Intelligence).\n2. **Executive Summary** : promesse produit, verdict technique, 5 score cards sur 10, argument de pourquoi ce projet peut être le meilleur, et alignement avec les contraintes/produits IDENTIFIEES dans le contexte.\n3. **Constat Profond** : diagnostic synthétique en 3-5 phrases fortes.\n4. **SWOT Stratégique** : tableau 2x2 (Forces, Faiblesses, Opportunités, Menaces).\n5. **Architecture & Tech Radar (Best-of-Breed)** : pour chaque technologie clé, explique pourquoi c'est le meilleur choix ici, donne un argument massue, un contre-argument, et une alternative classique.\n6. **Module Graph & Connexions** : hubs, feuilles, Mermaid, points de fragilité.\n7. **Sécurité & Confidentialité** : posture, cryptographie, vulnérabilités.\n8. **Qualité du Code & Dette** : signaux, top risques, correctifs.\n9. **Produit & UX** : parcours utilisateur, points de friction, idées d'amélioration.\n10. **Paysage Concurrentiel** : positionnement vs 3-4 acteurs, avantages différenciants, règles du jeu du marché.\n11. **Marketing & Positionnement** : persona cible, promesse unique (USP), tagline, canaux d'acquisition, argumentaire "pourquoi on va gagner", et **Master Move** : la feature/stratégie dominante qui fait gagner, compatible avec les contraintes du projet IDENTIFIEES dans le contexte.\n11b. **Contraintes Produits du Projet** (avant la roadmap) : liste les contraintes explicites identifiées dans == CONTRAINTES PRODUIT IDENTIFIEES == en début de contexte. Si aucune, indique "Aucune contrainte documentée". Cette section sert de référence pour justifier chaque action.\n12. **Roadmap 90 Jours & Justifications** : 4-6 actions concrètes priorisées (semaines 1-4, 5-8, 9-12) pour devenir le meilleur. Chaque action DOIT être suivie d'une phrase commençant par "> Justification :" qui explique pourquoi elle est cohérente avec les règles du projet. Si aucune contrainte, explique pourquoi elle est adaptée à la stack/architecture. Exemple : > Justification : Cette action respecte la règle "100% offline" car elle n'utilise aucun backend cloud.\n13. **Généré avec passion par shinzarou-eng** : concept produit/visuel original, slogan percutant, et argumentaire marketing gagnant inspiré par le code — dans le respect des contraintes du projet IDENTIFIEES dans le contexte.\n\nReste factuel, cible les fichiers par leur chemin relatif. Ne généralise pas hors du contexte. Si aucune contrainte produit n'est documentée, écris simplement "Contraintes produit : aucune" et continue toutes les sections normalement. NE SAUTE AUCUNE SECTION ET NE PERDS AUCUNE QUESTION. Réponds dans la langue de l'utilisateur.\n\n## CHECKLIST FINALE (obligatoire, vérifie avant d'envoyer)\n- [ ] Sections numérotées de 1 à 13.\n- [ ] Bannière ASCII en haut.\n- [ ] 5 score cards avec notes /10.\n- [ ] 1 diagramme Mermaid (architecture, data flow ou module graph).\n- [ ] Chaque action roadmap a un "> Justification :".\n- [ ] Au moins 2 citations de fichiers exactes (ligne).\n- [ ] Conclusion "Généré avec passion par shinzarou-eng".`;
1820
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${styleText}You are an **elite CTO / Technical Partner** writing the **ultimate Professional Assessment** for the project "${projectName}". This document must be the most beautiful, punchy and actionable: a board-ready high-level report. Mission: merge architecture, debt, competition, test/build results, git, marketing AND respect the product constraints IDENTIFIED in the context. Leverage the == PROJECT METRICS == and == KEY CODE SNIPPETS == to cite exact numbers and insert code snippets. Never impose constraints that are not in the context.${f}\n\n## Visual Identity (mandatory)${banner}\n- Use premium emojis for each section\n- Professional tables framed by Markdown lines\n- Mermaid diagrams\n- Quote boxes with > for insights and verdicts\n- "Score cards": Maturity, Security, Maintainability, Performance, UX (scores out of 10 with rationale)\n- Text badges: [CRITICAL], [HIGH-VALUE], [SECURITY], [STRATEGY], [BEST-TECH], [MARKETING], [RECOMMENDATION].\n- End with the signature block below:\n${signature}\n\n## Required Sections (number exactly 1 to 13)\n1. **Cover Page**: banner, date, project, version, author (DSH Codebase Intelligence).\n2. **Executive Summary**: product promise, technical verdict, 5 score cards out of 10, argument for why this project can be the best, and alignment with the product constraints IDENTIFIED in the context.\n3. **Deep Diagnosis**: synthetic diagnosis in 3-5 strong sentences.\n4. **Strategic SWOT**: 2x2 table (Strengths, Weaknesses, Opportunities, Threats).\n5. **Architecture & Tech Radar (Best-of-Breed)**: for each key technology, explain why it is the best choice here, give a hard-hitting argument, a counter-argument, and a classic alternative.\n6. **Module Graph & Connections**: hubs, leaves, Mermaid, fragility points.\n7. **Security & Privacy**: posture, cryptography, vulnerabilities.\n8. **Code Quality & Debt**: signals, top risks, fixes.\n9. **Product & UX**: user journey, friction points, improvement ideas.\n10. **Competitive Landscape**: positioning vs 3-4 players, differentiating advantages, market rules.\n11. **Marketing & Positioning**: target persona, unique selling proposition (USP), tagline, acquisition channels, "why we will win" argument, and **Master Move**: the dominant feature/strategy that makes you win, compatible with the product constraints IDENTIFIED in the context.\n11b. **Product Constraints of the Project** (before the roadmap): list the explicit constraints identified in == IDENTIFIED PRODUCT CONSTRAINTS == at the start of the context. If none, indicate "No documented constraints". This section serves as a reference to justify each action.\n12. **90-Day Roadmap & Rationale**: 4-6 prioritized concrete actions (weeks 1-4, 5-8, 9-12) to become the best. Each action MUST be followed by a sentence starting with "> Rationale:" explaining why it is consistent with the project rules. If no constraints, explain why it fits the stack/architecture. Example: > Rationale: This action respects the "100% offline" rule by not using any cloud backend.\n13. **Generated with passion by shinzarou-eng**: an original product/visual concept, a punchy slogan, and a winning marketing argument inspired by the code — respecting the product constraints IDENTIFIED in the context.\n\nStay factual, target files by their relative path. Do not generalize beyond the context. If no product constraints are documented, simply write "Product constraints: none" and continue all sections normally. DO NOT SKIP ANY SECTION AND DO NOT LOSE ANY QUESTION. Respond in the user\'s language.\n\n## FINAL CHECKLIST (mandatory, verify before sending)\n- [ ] Sections numbered 1 to 13.\n- [ ] ASCII banner at the top.\n- [ ] 5 score cards with scores out of 10.\n- [ ] 1 Mermaid diagram (architecture, data flow or module graph).\n- [ ] Each roadmap action has a "> Rationale:".\n- [ ] At least 2 exact file citations (line).\n- [ ] Conclusion "Generated with passion by shinzarou-eng".`;
1821
+ return withCitations(isEn ? en : fr, lang);
1822
+ }
1823
+
1824
+ function buildTasksPrompt(context, projectName, focus = "", style = "punchy", lang = "fr") {
1825
+ const isEn = lang === "en";
1826
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1827
+ const styleText = styleInstruction(style, lang);
1828
+ const banner = bannerInstruction(projectName, isEn ? "TASKS Action Plan" : "Plan d'Action TASKS", isEn ? "EXECUTABLE ROADMAP AND PRIORITIZED SPRINTS" : "ROADMAP EXÉCUTABLE ET SPRINTS PRIORISÉS");
1829
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${styleText}Tu es un **Delivery Lead / CTO** qui transforme un rapport de codebase en **plan d'action exécutable** pour le projet "${projectName}". RÈGLE D'OR : la section == TÂCHES GÉNÉRÉES DEPUIS LES SIGNAUX == contient déjà les tâches avec leurs blocs Avant/Après. Tu DOIS les recopier TELLES QUELLES dans le TASKS.md final, les organiser en sprints (P0, P1, P2, P3), et conserver OBLIGATOIREMENT les blocs Avant et Après FOURNIS. Tu as le droit d'ajouter 2-3 tâches maximum si tu identifies un risque majeur absent, mais la majorité du plan doit venir des tâches pré-générées. Chaque tâche doit être compatible avec les contraintes produit IDENTIFIEES dans le contexte (README, AGENTS.md, MEMORY.md, package.json). Avant la liste des tâches, ajoute une section "Contraintes produit identifiées" pour servir de référence.${f}\n\n## Ton et style${banner}\n- Un titre clair :\n~~~markdown\n# TASKS.md — Plan d'action ${projectName}\n~~~\n- Des tableaux avec colonnes : Priorité, Tâche, Fichier(s) concerné(s), Difficulté (1-5), Impact, Livrable\n- Des checklists Markdown : '[ ]' / '[x]'\n- Des sprints : Sprint 1 (semaines 1-2), Sprint 2, Sprint 3\n- Des badges : [CRITIQUE], [RAPIDE], [STRATEGIQUE], [TECH-DEBT].\n- Conclus par : Généré avec passion par shinzarou-eng (dans la langue de l'utilisateur)\n\n## Sections obligatoires\n1. **Vue d'ensemble** : 3-5 tâches prioritaires dans un tableau.\n2. **Sprint 1 — Fondations** : sécurité, stabilité, tests.\n3. **Sprint 2 — Amélioration** : refacto, UX, performance.\n4. **Sprint 3 — Différenciation** : features gagnantes, marketing.\n5. **Checklist globale** : toutes les tâches avec '[ ]'.\n\nChaque tâche doit être actionnable, citer un chemin de fichier relatif quand c'est possible, et être suivie d'une phrase commençant par "> Justification :" qui explique pourquoi elle est cohérente avec les règles du projet.\n\n## Exigence AVANT / APRÈS\nPour CHAQUE tâche, ajoute obligatoirement deux blocs de code :\n- **Avant** : extrait du code actuel (max 10 lignes) depuis le contexte == TECH DEBT & SIGNALS == ou == EXTRAITS DE CODE CLÉS ==.\n- **Après** : extrait du code corrigé proposé (max 10 lignes).\n\nExemple de format :\n- [ ] **[TASK-01] Typer l'événement SpeechRecognition**\n - Fichier : src/components/KodaAssistantModal.tsx:651\n - Avant :\n --- code ts ---\n recognition.onresult = (event: any) => { ... };\n ---\n - Après :\n --- code ts ---\n recognition.onresult = (event: SpeechRecognitionEvent) => { ... };\n ---\n > Justification : ...\n\nSi tu ne peux pas extraire l'extrait, cite au minimum le fichier et la ligne.\n\n## CHECKLIST FINALE (obligatoire, vérifie avant d'envoyer)\n- [ ] 5 sections présentes (Vue d'ensemble, Sprint 1, Sprint 2, Sprint 3, Checklist globale).\n- [ ] Toutes les tâches ont un statut '[ ]'.\n- [ ] Chaque tâche a un bloc **Avant** (code actuel) et un bloc **Après** (code proposé), ou 'N/A' avec explication.\n- [ ] Chaque tâche a un Fichier:Ligne.\n- [ ] Chaque tâche a un "> Justification :".\n- [ ] Conclusion "Généré avec passion par shinzarou-eng".\n\nSi aucune contrainte n'est trouvée, écris "Contraintes produit : aucune" et continue toutes les sections normalement. NE SAUTE AUCUNE SECTION ET NE PERDS AUCUNE QUESTION.`;
1830
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${styleText}You are a **Delivery Lead / CTO** turning a codebase report into an **executable action plan** for the project "${projectName}". GOLDEN RULE: the section == TASKS GENERATED FROM SIGNALS == already contains the tasks with their Before/After blocks. You MUST copy them AS-IS into the final TASKS.md, organize them into sprints (P0, P1, P2, P3), and OBLIGATORILY keep the provided Before and After blocks. You may add 2-3 extra tasks at most if you identify a major missing risk, but the majority of the plan must come from the pre-generated tasks. Each task must be compatible with the product constraints IDENTIFIED in the context (README, AGENTS.md, MEMORY.md, package.json). Before the task list, add an "Identified product constraints" section as a reference.${f}\n\n## Tone & Style${banner}\n- A clear title:\n~~~markdown\n# TASKS.md — Action Plan ${projectName}\n~~~\n- Tables with columns: Priority, Task, Concerned file(s), Difficulty (1-5), Impact, Deliverable\n- Markdown checklists: '[ ]' / '[x]'\n- Sprints: Sprint 1 (weeks 1-2), Sprint 2, Sprint 3\n- Badges: [CRITICAL], [QUICK], [STRATEGIC], [TECH-DEBT].\n- Conclude with: Generated with passion by shinzarou-eng (in the user\'s language)\n\n## Required Sections\n1. **Overview**: 3-5 prioritized tasks in a table.\n2. **Sprint 1 — Foundations**: security, stability, tests.\n3. **Sprint 2 — Improvement**: refactor, UX, performance.\n4. **Sprint 3 — Differentiation**: winning features, marketing.\n5. **Global Checklist**: all tasks with '[ ]'.\n\nEach task must be actionable, cite a relative file path when possible, and be followed by a sentence starting with "> Rationale:" explaining why it is consistent with the project rules.\n\n## BEFORE / AFTER Requirement\nFor EACH task, you MUST add two code blocks:\n- **Before**: current code snippet (max 10 lines) from == TECH DEBT & SIGNALS == or == KEY CODE SNIPPETS == in the context.\n- **After**: proposed fixed code snippet (max 10 lines).\n\nExample format:\n- [ ] **[TASK-01] Type the SpeechRecognition event**\n - File: src/components/KodaAssistantModal.tsx:651\n - Before:\n --- code ts ---\n recognition.onresult = (event: any) => { ... };\n ---\n - After:\n --- code ts ---\n recognition.onresult = (event: SpeechRecognitionEvent) => { ... };\n ---\n > Rationale : ...\n\nIf you cannot extract the snippet, at least cite the file and line.\n\n## FINAL CHECKLIST (mandatory, verify before sending)\n- [ ] 5 sections present (Overview, Sprint 1, Sprint 2, Sprint 3, Global Checklist).\n- [ ] All tasks have status '[ ]'.\n- [ ] Each task has a **Before** (current code) and an **After** (proposed code) block, or 'N/A' with explanation.\n- [ ] Each task has a File:Line.\n- [ ] Each task has a "> Rationale:".\n- [ ] Conclusion "Generated with passion by shinzarou-eng".\n\nIf no constraints are found, write "Product constraints: none" and continue all sections normally. DO NOT SKIP ANY SECTION AND DO NOT LOSE ANY QUESTION.`;
1831
+ return withCitations(isEn ? en : fr, lang);
1832
+ }
1833
+
1834
+ function buildCeoPrompt(context, projectName, focus = "", lang = "fr") {
1835
+ const isEn = lang === "en";
1836
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1837
+ const styleText = styleInstruction("ouf", lang);
1838
+ const signature = brandSignature();
1839
+ const banner = bannerInstruction(projectName, isEn ? "One-Page CEO Brief" : "One-Page CEO Brief", isEn ? "EXECUTIVE SUMMARY FOR DECISION MAKERS" : "EXECUTIVE SUMMARY POUR DÉCIDEUR");
1840
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${styleText}Tu es un **CTO / CEO / Partner** qui rédige le **One-Page Executive Brief** ultime sur le projet "${projectName}". RÈGLE D'OR : ce document tient sur UNE SEULE PAGE A4. MAXIMUM 7 sections courtes. Pas de blabla, que des insights à fort impact, des chiffres, des verdicts, des actions. Chaque section max 5-8 lignes. Utilise tableaux et listes. Si tu dépasses une page, tu as échoué.${f}\n\n## Identité visuelle (obligatoire)${banner}\n- Tableau exécutif unique avec les metrics clés.\n- 5 score cards (sur 10) avec justification en UNE phrase.\n- Encadrés visuels pour les insights.\n- Badges : [CRITIQUE], [HIGH-VALUE], [STRATEGY], [BEST-TECH], [KILLER-MOVE], [MARKETING].\n- Conclus par le bloc signature ci-dessous :\n${signature}\n\n## Sections obligatoires (numérote de 1 à 7, STRICTEMENT 1 PAGE)\n1. **Bannière & Titre** : 1 ligne.\n2. **Executive Summary** : 3 phrases + 1 tableau 4 métriques.\n3. **Verdict du board** : 5 score cards, 1 ligne chacune (Domaine | Note | 5 mots de justification).\n4. **Top 3 risques / dette** : 1 ligne par risque (Fichier:Ligne - Problème - Impact).\n5. **Top 3 opportunités / Killer Moves** : 1 phrase par action + Justification en 1 phrase.\n6. **SWOT ultra-concis** : 4 cases, max 4 points de 3-5 mots chacun.\n7. **Généré avec passion par shinzarou-eng** : 1 concept + 1 slogan + 1 phrase marketing.\n\nReste factuel, cible les fichiers par leur chemin relatif. Exploite les == MÉTRIQUES PROJET == et == EXTRAITS DE CODE CLÉS ==. Réponds dans la langue de l'utilisateur.\n\n## CHECKLIST FINALE (obligatoire, vérifie avant d'envoyer)\n- [ ] Exactement 7 sections numérotées.\n- [ ] Le document tient sur une page (max 60-80 lignes au total).\n- [ ] 5 score cards, 1 ligne chacune.\n- [ ] Top 3 risques avec Fichier:Ligne.\n- [ ] Top 3 opportunités avec "> Justification :".\n- [ ] SWOT : 4 points de 3-5 mots par case.\n- [ ] Aucun Mermaid, aucun tableau géant, aucune explication longue.`;
1841
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${styleText}You are a **CTO / CEO / Partner** writing the ultimate **One-Page Executive Brief** for the project "${projectName}". GOLDEN RULE: this document fits on a SINGLE A4 page. MAXIMUM 7 short sections. No filler, only high-impact insights, numbers, verdicts, actions. Each section max 5-8 lines. Use tables and lists. If you exceed one page, you failed.${f}\n\n## Visual Identity (mandatory)${banner}\n- Single executive table with key metrics.\n- 5 score cards (out of 10) with rationale in ONE sentence.\n- Visual callouts for insights.\n- Badges: [CRITICAL], [HIGH-VALUE], [STRATEGY], [BEST-TECH], [KILLER-MOVE], [MARKETING].\n- Conclude with the signature block below:\n${signature}\n\n## Required Sections (number 1 to 7, STRICTLY 1 PAGE)\n1. **Banner & Title**: 1 line.\n2. **Executive Summary**: 3 sentences + 1 table with 4 metrics.\n3. **Board verdict**: 5 score cards, 1 line each (Area | Score | 5-word rationale).\n4. **Top 3 risks / debt**: 1 line per risk (File:Line - Problem - Impact).\n5. **Top 3 opportunities / Killer Moves**: 1 sentence per action + Rationale in 1 sentence.\n6. **Ultra-concise SWOT**: 4 boxes, max 4 points of 3-5 words each.\n7. **Generated with passion by shinzarou-eng**: 1 concept + 1 slogan + 1 marketing sentence.\n\nStay factual, target files by their relative path. Leverage == PROJECT METRICS == and == KEY CODE SNIPPETS ==. Respond in the user\'s language.\n\n## FINAL CHECKLIST (mandatory, verify before sending)\n- [ ] Exactly 7 numbered sections.\n- [ ] Document fits on one page (max 60-80 lines total).\n- [ ] 5 score cards, 1 line each.\n- [ ] Top 3 risks with File:Line.\n- [ ] Top 3 opportunities with "> Rationale:".\n- [ ] SWOT: 4 points of 3-5 words per box.\n- [ ] No Mermaid, no giant table, no long explanation.`;
1842
+ return withCitations(isEn ? en : fr, lang);
1843
+ }
1844
+
1845
+ function buildBuildPrompt(context, projectName, focus = "", lang = "fr") {
1846
+ const isEn = lang === "en";
1847
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1848
+ const banner = bannerInstruction(projectName, isEn ? "Build Benchmark" : "Build Benchmark", isEn ? "BUILD PERFORMANCE & BUNDLE" : "PERFORMANCE DE BUILD & BUNDLE");
1849
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nTu es un **Staff Engineer / Build Performance Expert**. Analyse le résultat du build du projet "${projectName}". Produis un rapport ultra-concis et percutant.${f}\n\n## Format attendu\n- Bannière ASCII : "BUILD BENCHMARK — ${(projectName ?? "").toUpperCase()}"\n- 3 score cards : Vitesse, Taille du bundle, Stabilité (notes /10)\n- Tableau des fichiers de sortie (nom, taille)\n- Top 3 leviers d'optimisation\n- Conclusion : Généré avec passion par shinzarou-eng (langue utilisateur)`;
1850
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nYou are a **Staff Engineer / Build Performance Expert**. Analyze the build result of the project "${projectName}". Produce an ultra-concise, punchy report.${f}\n\n## Expected format\n- ASCII banner: "BUILD BENCHMARK — ${(projectName ?? "").toUpperCase()}"\n- 3 score cards: Speed, Bundle size, Stability (scores out of 10)\n- Output files table (name, size)\n- Top 3 optimization levers\n- Conclusion: Generated with passion by shinzarou-eng (user language)`;
1851
+ return withCitations(isEn ? en : fr, lang);
1852
+ }
1853
+
1854
+ function buildPlayerPrompt(context, projectName, focus = "", lang = "fr") {
1855
+ const isEn = lang === "en";
1856
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1857
+ const banner = bannerInstruction(projectName, isEn ? "Player Brief" : "Player Brief", isEn ? "USER JOURNEY & EXPERIENCE" : "PARCOURS UTILISATEUR & EXPÉRIENCE");
1858
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nTu es un **UX Researcher / Playtester / Product Hunter** qui réalise un **Player Brief** sur le projet "${projectName}". Tu ne regardes pas le code comme un dev, mais comme un vrai utilisateur final qui découvre l'app, clique, se frustre, se réjouit. Mission : décrire l'expérience vécue, identifier les moments clés, les frictions et les opportunités de 'wow'.${f}\n\n## Format attendu\n- Bannière ASCII : "PLAYER BRIEF — ${(projectName ?? "").toUpperCase()}"\n- Score cards : Onboarding, Clarté, Réactivité, Confiance, Plaisir (sur 10)\n- Tableau du parcours utilisateur : Étape, Action, Sentiment, Friction, Fix\n- Top 5 moments 'Wow' (ce qui impressionne)\n- Top 5 frictions bloquantes ou irritantes\n- Idées de gamification / engagement (si pertinent)\n- Roadmap UX 30 jours : 3 actions rapides d'impact utilisateur\n- Conclusion : Généré avec passion par shinzarou-eng (langue utilisateur)`;
1859
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nYou are a **UX Researcher / Playtester / Product Hunter** producing a **Player Brief** for the project "${projectName}". You do not look at the code like a dev, but like a real end user discovering the app, clicking, getting frustrated, getting delighted. Mission: describe the lived experience, identify key moments, frictions and 'wow' opportunities.${f}\n\n## Expected format\n- ASCII banner: "PLAYER BRIEF — ${(projectName ?? "").toUpperCase()}"\n- Score cards: Onboarding, Clarity, Responsiveness, Trust, Delight (out of 10)\n- User journey table: Step, Action, Sentiment, Friction, Fix\n- Top 5 'Wow' moments (what impresses)\n- Top 5 blocking or annoying frictions\n- Gamification / engagement ideas (if relevant)\n- 30-day UX roadmap: 3 quick high-impact actions\n- Conclusion: Generated with passion by shinzarou-eng (user language)`;
1860
+ return withCitations(isEn ? en : fr, lang);
1861
+ }
1862
+
1863
+ function buildGitPrompt(context, projectName, focus = "", lang = "fr") {
1864
+ const isEn = lang === "en";
1865
+ const f = focus ? (isEn ? `\nRequested focus: ${focus}` : `\nFocus demandé : ${focus}`) : "";
1866
+ const banner = bannerInstruction(projectName, isEn ? "Git Intelligence" : "Git Intelligence", isEn ? "HISTORY, HOTSPOTS & RISKS" : "HISTORIQUE, HOTSPOTS & RISQUES");
1867
+ const fr = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nTu es un **Tech Lead** qui analyse l'historique git du projet "${projectName}". Résume l'activité, identifie les tendances, les hotspots de modification et les risques récents.${f}\n\n## Format attendu\n- Bannière ASCII : "GIT INTELLIGENCE — ${(projectName ?? "").toUpperCase()}"\n- Derniers commits synthétisés\n- Hotspots (fichiers qui bougent le plus)\n- Risques récents\n- Suggestions de prochaines actions\n- Conclusion : Généré avec passion par shinzarou-eng (langue utilisateur)`;
1868
+ const en = `${context}\n\n${langInstruction(lang)}\n\n${banner}\n\nYou are a **Tech Lead** analyzing the git history of the project "${projectName}". Summarize activity, identify trends, modification hotspots and recent risks.${f}\n\n## Expected format\n- ASCII banner: "GIT INTELLIGENCE — ${(projectName ?? "").toUpperCase()}"\n- Synthesized recent commits\n- Hotspots (files that change the most)\n- Recent risks\n- Suggested next actions\n- Conclusion: Generated with passion by shinzarou-eng (user language)`;
1869
+ return withCitations(isEn ? en : fr, lang);
1870
+ }
1871
+
1872
+ function buildApplyPrompt(filePath, newContent, projectName, lang = "fr") {
1873
+ const isEn = lang === "en";
1874
+ const fr = `Applique la mise à jour suivante au fichier "${filePath}" du projet "${projectName}".\n\n${langInstruction(lang)}\n\nTu es un **Staff Engineer**. Le contenu fourni est la nouvelle version complète du fichier. Réponds UNIQUEMENT par :\n\n- Si le patch semble correct : "Fichier ${filePath} mis à jour avec succès."\n- Si tu refuses : dis pourquoi en une phrase.\n\nGénéré avec passion par shinzarou-eng.\n\n---\n\nNouveau contenu :\n~~~\n${newContent.slice(0, 8000)}\n~~~`;
1875
+ const en = `Apply the following update to file "${filePath}" in project "${projectName}".\n\n${langInstruction(lang)}\n\nYou are a **Staff Engineer**. The provided content is the complete new version of the file. Respond ONLY with:\n\n- If the patch looks correct: "File ${filePath} updated successfully."\n- If you refuse: say why in one sentence.\n\nGenerated with passion by shinzarou-eng.\n\n---\n\nNew content:\n~~~\n${newContent.slice(0, 8000)}\n~~~`;
1876
+ return withCitations(isEn ? en : fr, lang);
1877
+ }
1878
+
1879
+ function isDirectoryLike(value) {
1880
+ if (!value) return false;
1881
+ try {
1882
+ const expanded = value.replace(/^~/, homedir());
1883
+ const resolved = resolve(expanded);
1884
+ return existsSync(resolved) && statSync(resolved).isDirectory();
1885
+ } catch {
1886
+ return false;
1887
+ }
1888
+ }
1889
+
1890
+ function pullLeadingPath(input) {
1891
+ const quoted = input.match(/^"([^"]+)"(?:\s+(.*))?$/s) || input.match(/^'([^']+)'(?:\s+(.*))?$/s);
1892
+ if (quoted) {
1893
+ const candidate = quoted[1];
1894
+ if (isDirectoryLike(candidate)) {
1895
+ return { path: candidate, rest: quoted[2] || "" };
1896
+ }
1897
+ return null;
1898
+ }
1899
+
1900
+ const token = input.match(/^(\S+)(?:\s+(.*))?$/s);
1901
+ if (token && isDirectoryLike(token[1])) {
1902
+ return { path: token[1], rest: token[2] || "" };
1903
+ }
1904
+ return null;
1905
+ }
1906
+
1907
+ function parseCodebaseInput(rawInput) {
1908
+ let input = rawInput.trim();
1909
+ const result = { projectPath: "", filePath: "", query: "", style: "ouf", crea: false, creaTheme: "", raw: false, apply: false, dryRun: false };
1910
+
1911
+ // Le premier argument positionnel peut etre un chemin de projet (ex: /codebase "D:\\mon app")
1912
+ const leading = pullLeadingPath(input);
1913
+ if (leading) {
1914
+ result.projectPath = leading.path;
1915
+ input = leading.rest.trim();
1916
+ }
1917
+
1918
+ const projectFlag = input.match(/--(?:project|projet|path|p)\s+((?:"[^"]+")|(?:'[^']+')|(?:\S+))/i);
1919
+ if (projectFlag) {
1920
+ result.projectPath = projectFlag[1].replace(/^["']|["']$/g, "");
1921
+ input = input.replace(projectFlag[0], "").trim();
1922
+ }
1923
+
1924
+ const fileFlag = input.match(/--(?:file|fichier|f)\s+((?:"[^"]+")|(?:'[^']+')|(?:\S+))/i);
1925
+ if (fileFlag) {
1926
+ result.filePath = fileFlag[1].replace(/^["']|["']$/g, "");
1927
+ input = input.replace(fileFlag[0], "").trim();
1928
+ }
1929
+
1930
+ const styleFlag = input.match(/--(?:style|s)\s+((?:"[^"]+")|(?:'[^']+')|(?:[^-\s][^\s]*))/i);
1931
+ if (styleFlag) {
1932
+ const rawStyle = styleFlag[1].replace(/^["']|["']$/g, "").toLowerCase().trim();
1933
+ result.style = { wow: "ouf", ouf: "ouf", best: "ouf" }[rawStyle] || rawStyle;
1934
+ input = input.replace(styleFlag[0], "").trim();
1935
+ }
1936
+
1937
+ const creaThemeFlag = input.match(/--(?:crea-theme|theme|t)\s+((?:"[^"]+")|(?:'[^']+')|(?:[^-\s][^\s]*))/i);
1938
+ if (creaThemeFlag) {
1939
+ result.creaTheme = creaThemeFlag[1].replace(/^["']|["']$/g, "");
1940
+ input = input.replace(creaThemeFlag[0], "").trim();
1941
+ }
1942
+
1943
+ const creaFlag = input.match(/--(?:crea|c)(?:\s|$)/i);
1944
+ if (creaFlag) {
1945
+ result.crea = true;
1946
+ input = input.replace(creaFlag[0], "").trim();
1947
+ }
1948
+
1949
+ const rawFlag = input.match(/--(?:raw|r)(?:\s|$)/i);
1950
+ if (rawFlag) {
1951
+ result.raw = true;
1952
+ input = input.replace(rawFlag[0], "").trim();
1953
+ }
1954
+
1955
+ const applyFlag = input.match(/--(?:apply|a)(?:\s|$)/i);
1956
+ if (applyFlag) {
1957
+ result.apply = true;
1958
+ input = input.replace(applyFlag[0], "").trim();
1959
+ }
1960
+
1961
+ const dryRunFlag = input.match(/--(?:dry-run|dryrun|d)(?:\s|$)/i);
1962
+ if (dryRunFlag) {
1963
+ result.dryRun = true;
1964
+ input = input.replace(dryRunFlag[0], "").trim();
1965
+ }
1966
+
1967
+ const quoted = input.match(/^"([^"]+)"(?:\s+(.*))?$/s) || input.match(/^'([^']+)'(?:\s+(.*))?$/s);
1968
+ if (quoted) {
1969
+ result.query = quoted[1];
1970
+ } else {
1971
+ result.query = input;
1972
+ }
1973
+ return result;
1974
+ }
1975
+
1976
+ async function submitToAgent(ctx, sessionId, text, signal) {
1977
+ let agent;
1978
+ try {
1979
+ const agents = ctx.get("agents");
1980
+ agent = agents.get(sessionId);
1981
+ } catch { }
1982
+ if (agent === void 0) {
1983
+ try {
1984
+ const sessions = ctx.get("sessions");
1985
+ const session = sessions.get(sessionId);
1986
+ if (session !== void 0) {
1987
+ const agents = ctx.get("agents");
1988
+ for (const [id, a] of agents.entries()) {
1989
+ if (a?.sessionId === sessionId || a?.id === sessionId) {
1990
+ agent = a;
1991
+ break;
1992
+ }
1993
+ }
1994
+ if (agent === void 0) {
1995
+ if (typeof session.steer === "function") {
1996
+ agent = session;
1997
+ } else {
1998
+ throw new Error("Aucune session active. Ouvre ou cree une session DSH d'abord.");
1999
+ }
2000
+ }
2001
+ }
2002
+ } catch (err) {
2003
+ if (err instanceof Error && err.message.includes("Aucune session active")) throw err;
2004
+ }
2005
+ }
2006
+ if (agent === void 0) throw new Error("Aucune session active. Ouvre ou cree une session DSH d'abord.");
2007
+ agent.steer(createUserMessage({
2008
+ content: [{ type: "text", text }],
2009
+ source: { kind: "user" }
2010
+ }));
2011
+ return { ok: true };
2012
+ }
2013
+
2014
+ function registerTool(tools, name, description, parameters, renderFn, executeFn) {
2015
+ tools.register(defineTool({
2016
+ name,
2017
+ description,
2018
+ parameters,
2019
+ output: {
2020
+ schema: {
2021
+ type: "object",
2022
+ properties: {
2023
+ context: { type: "string" },
2024
+ query: { type: "string" },
2025
+ description: { type: "string" },
2026
+ projectName: { type: "string" },
2027
+ crea: { type: "boolean" },
2028
+ creaTheme: { type: "string" }
2029
+ },
2030
+ additionalProperties: false
2031
+ },
2032
+ render(_args, value) {
2033
+ return [{ type: "text", text: renderFn(value) }];
2034
+ }
2035
+ },
2036
+ timeoutMs: 60_000,
2037
+ execute: executeFn
2038
+ }));
2039
+ }
2040
+
2041
+ function apply(ctx) {
2042
+ const tools = ctx.get("tools");
2043
+ const commands = ctx.get("commands");
2044
+ const agents = ctx.get("agents");
2045
+ const systemPrompt = ctx.get("systemPrompt");
2046
+
2047
+ registerTool(
2048
+ tools,
2049
+ "codebase_chat",
2050
+ "Answer a question about a local codebase by reading project files. Use when the user asks about code, architecture, how something works, or references a project folder. Supports any language and an optional creative footer. Set crea=true to append a creative idea at the end.",
2051
+ {
2052
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2053
+ question: { type: "string", description: "Question about the codebase." },
2054
+ filePath: { type: "string", description: "Optional specific file or symbol to focus on." },
2055
+ crea: { type: "boolean", description: "If true, append a creative 'Généré avec passion par shinzarou-eng <project>' footer." },
2056
+ creaTheme: { type: "string", description: "Optional creative theme for the footer." }
2057
+ },
2058
+ (value) => buildChatPrompt(value.query, value.context, value.projectName, value.crea, value.creaTheme),
2059
+ async (args, exec) => {
2060
+ const { absProject, context } = await collectCodebaseContext(args.projectPath, { focus: args.question, filePath: args.filePath });
2061
+ const projectName = await getProjectName(absProject);
2062
+ return { query: args.question, projectName, crea: args.crea || false, creaTheme: args.creaTheme || "", context };
2063
+ }
2064
+ );
2065
+
2066
+ registerTool(
2067
+ tools,
2068
+ "codebase_search",
2069
+ "Search for a term, pattern, or concept across a local codebase and summarize where it is used. Use for 'where is X used', 'find references', or 'search for'. Answer in the user's language. Set crea=true to append a creative idea at the end.",
2070
+ {
2071
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2072
+ query: { type: "string", description: "Term, symbol, or pattern to search." },
2073
+ crea: { type: "boolean", description: "If true, append a creative 'Généré avec passion par shinzarou-eng <project>' footer." },
2074
+ creaTheme: { type: "string", description: "Optional creative theme for the footer." }
2075
+ },
2076
+ (value) => buildSearchPrompt(value.query, value.context, value.projectName, value.crea, value.creaTheme),
2077
+ async (args, exec) => {
2078
+ const { absProject, context } = await collectCodebaseContext(args.projectPath, { focus: args.query, searchQuery: args.query });
2079
+ const projectName = await getProjectName(absProject);
2080
+ return { query: args.query, projectName, crea: args.crea || false, creaTheme: args.creaTheme || "", context };
2081
+ }
2082
+ );
2083
+
2084
+ registerTool(
2085
+ tools,
2086
+ "codebase_explain",
2087
+ "Explain a specific file, function, class, or symbol in a local codebase. Use when the user asks 'explain X' or 'what does Y do'. Answer in the user's language. Set crea=true to append a creative idea at the end.",
2088
+ {
2089
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2090
+ target: { type: "string", description: "File name, relative path, or symbol to explain." },
2091
+ crea: { type: "boolean", description: "If true, append a creative 'Généré avec passion par shinzarou-eng <project>' footer." },
2092
+ creaTheme: { type: "string", description: "Optional creative theme for the footer." }
2093
+ },
2094
+ (value) => buildExplainPrompt(value.query, value.context, value.projectName, value.crea, value.creaTheme),
2095
+ async (args, exec) => {
2096
+ const { absProject, context } = await collectCodebaseContext(args.projectPath, { focus: args.target, filePath: args.target });
2097
+ const projectName = await getProjectName(absProject);
2098
+ return { query: args.target, projectName, crea: args.crea || false, creaTheme: args.creaTheme || "", context };
2099
+ }
2100
+ );
2101
+
2102
+ registerTool(
2103
+ tools,
2104
+ "codebase_refactor",
2105
+ "Propose a refactor for a specific file in a local codebase based on a description. Returns the suggested changes. Answer in the user's language. Set crea=true to append a creative idea at the end.",
2106
+ {
2107
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2108
+ filePath: { type: "string", description: "File name or relative path to refactor." },
2109
+ description: { type: "string", description: "What to change and why." },
2110
+ crea: { type: "boolean", description: "If true, append a creative 'Généré avec passion par shinzarou-eng <project>' footer." },
2111
+ creaTheme: { type: "string", description: "Optional creative theme for the footer." }
2112
+ },
2113
+ (value) => buildRefactorPrompt(value.query, value.description, value.context, value.projectName, value.crea, value.creaTheme),
2114
+ async (args, exec) => {
2115
+ const { absProject, context } = await collectCodebaseContext(args.projectPath, { focus: args.description, filePath: args.filePath });
2116
+ const projectName = await getProjectName(absProject);
2117
+ return { query: args.filePath, description: args.description || "", projectName, crea: args.crea || false, creaTheme: args.creaTheme || "", context };
2118
+ }
2119
+ );
2120
+
2121
+ registerTool(
2122
+ tools,
2123
+ "codebase_crea",
2124
+ "Generate a creative output (slogan, feature name, tagline, visual concept, one-liner) based on the analyzed codebase. Useful for marketing, naming, or product ideation.",
2125
+ {
2126
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2127
+ theme: { type: "string", description: "Optional creative theme (e.g. 'slogan for onboarding', 'feature name for reminders')." }
2128
+ },
2129
+ (value) => buildCreaPrompt(value.query, value.context, value.projectName),
2130
+ async (args, exec) => {
2131
+ const { absProject, context } = await collectCodebaseContext(args.projectPath, { focus: args.theme || "creative output" });
2132
+ const projectName = await getProjectName(absProject);
2133
+ return { query: args.theme || "creative", projectName, context };
2134
+ }
2135
+ );
2136
+
2137
+ registerTool(
2138
+ tools,
2139
+ "codebase_intelligence",
2140
+ "Run a full professional intelligence brief on a codebase: architecture, module graph, data flow, risks, opportunities, and a creative idea. Use when the user wants an overview, audit, architect view, or just clicks the Codebase button. Answer in the user's language.",
2141
+ {
2142
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2143
+ focus: { type: "string", description: "Optional focus for the intelligence brief." }
2144
+ },
2145
+ (value) => buildIntelligencePrompt(value.context, value.projectName, value.query),
2146
+ async (args, exec) => {
2147
+ const { absProject, context } = await collectIntelligenceContext(args.projectPath, args.focus || "professional intelligence brief");
2148
+ const projectName = await getProjectName(absProject);
2149
+ return { query: args.focus || "intelligence", projectName, context };
2150
+ }
2151
+ );
2152
+
2153
+ registerTool(
2154
+ tools,
2155
+ "codebase_audit",
2156
+ "Audit a codebase for non-conformities, technical debt, errors, TODO/FIXME, console.log, bare catch, ts-ignore, any types, eval, innerHTML, and debugger. Proposes concrete fixes and a prioritized action plan. Use when the user asks for an audit, errors, debt, cleanup, or non-conformites.",
2157
+ {
2158
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2159
+ focus: { type: "string", description: "Optional focus (e.g. security, types, console)." }
2160
+ },
2161
+ (value) => buildAuditPrompt(value.context, value.projectName, value.query),
2162
+ async (args, exec) => {
2163
+ const { context, projectName } = await collectNonConformities(args.projectPath, args.focus || "");
2164
+ return { query: args.focus || "audit", projectName, context };
2165
+ }
2166
+ );
2167
+
2168
+ registerTool(
2169
+ tools,
2170
+ "codebase_report",
2171
+ "Generate a deep professional strategic report (constat professionnel) combining architecture, audit, security, SWOT, score cards, competitor landscape, and a 90-day roadmap. Use when the user asks for a report, constat, strategic view, executive summary, or wants the most beautiful and complete output.",
2172
+ {
2173
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2174
+ focus: { type: "string", description: "Optional focus for the strategic report." }
2175
+ },
2176
+ (value) => buildReportPrompt(value.context, value.projectName, value.query),
2177
+ async (args, exec) => {
2178
+ const { context, projectName } = await collectAssessmentContext(args.projectPath, args.focus || "rapport stratégique");
2179
+ return { query: args.focus || "report", projectName, context };
2180
+ }
2181
+ );
2182
+
2183
+ registerTool(
2184
+ tools,
2185
+ "codebase_tasks",
2186
+ "Generate an actionable TASKS.md plan from a codebase assessment. Use when the user wants a sprint plan, roadmap, or task list.",
2187
+ {
2188
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2189
+ focus: { type: "string", description: "Optional focus (e.g. security, performance, product)." },
2190
+ raw: { type: "boolean", description: "If true, write a raw TASKS.md directly to the project and return the markdown without LLM rewriting." },
2191
+ apply: { type: "boolean", description: "If true and raw is true, apply the patches automatically." },
2192
+ dryRun: { type: "boolean", description: "If true and apply is true, show what would change without writing files." }
2193
+ },
2194
+ (value) => {
2195
+ if (value.markdown) return value.markdown;
2196
+ if (value.applyReport) return value.applyReport;
2197
+ return buildTasksPrompt(value.context, value.projectName, value.query);
2198
+ },
2199
+ async (args, exec) => {
2200
+ const { absProject, context, projectName, preTasks, metrics, constraints } = await collectTasksContext(args.projectPath, args.focus || "plan d'action");
2201
+ if (args.raw) {
2202
+ if (isProtectedPath(absProject)) throw new Error("Ce chemin est protege. TASKS.md n'a pas ete ecrit.");
2203
+ const markdown = formatRawTasksMarkdown(projectName, metrics, constraints, preTasks);
2204
+ const tasksPath = join(absProject, "TASKS.md");
2205
+ await writeFile(tasksPath, markdown, "utf8");
2206
+ if (args.apply) {
2207
+ const result = await runApplyTasks(absProject, markdown, args.dryRun);
2208
+ const report = `Application des taches (dryRun=${args.dryRun}) :\n- ${result.message}\n- Fichiers touches : ${(result.touchedFiles || []).join(", ") || "aucun"}\n- Resultats detailles :\n${(result.results || []).map((r) => ` - ${r.id || r.file || ""} ligne ${r.line} : ${r.status}${r.status === "mismatch" ? ` (attendu: ${r.expected?.slice(0, 40)}..., trouve: ${r.got?.slice(0, 40)}...)` : ""}`).join("\n")}\n${result.verification ? `Verification post-apply :\n${result.verification.map((v) => ` - ${v.name} : ${v.ok ? "OK" : "KO"}\n${v.output.slice(-200)}`).join("\n")}` : ""}`;
2209
+ return { query: args.focus || "tasks", projectName, context, applyReport: report };
2210
+ }
2211
+ return { query: args.focus || "tasks", projectName, context, markdown: `${tasksPath}\n\n${markdown}` };
2212
+ }
2213
+ return { query: args.focus || "tasks", projectName, context };
2214
+ }
2215
+ );
2216
+
2217
+ registerTool(
2218
+ tools,
2219
+ "codebase_apply_tasks",
2220
+ "Apply a previously generated TASKS.md to the project source code. Use when the user asks to apply tasks, execute the plan, or run the fixes. Supports dry-run to preview changes.",
2221
+ {
2222
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2223
+ dryRun: { type: "boolean", description: "If true, show planned changes without writing files." },
2224
+ tasksFile: { type: "string", description: "Path to the TASKS.md file (default: PROJECT/TASKS.md)." }
2225
+ },
2226
+ (value) => value.applyReport || "Rapport d'application des taches.",
2227
+ async (args, exec) => {
2228
+ const absProject = await resolveProjectPath(args.projectPath);
2229
+ const tasksPath = args.tasksFile ? resolve(absProject, args.tasksFile) : join(absProject, "TASKS.md");
2230
+ const markdown = await safeReadText(tasksPath);
2231
+ if (!markdown) throw new Error(`TASKS.md introuvable : ${tasksPath}. Generez-le d'abord avec codebase_tasks raw=true.`);
2232
+ const result = await runApplyTasks(absProject, markdown, args.dryRun);
2233
+ const report = `Application des taches (dryRun=${args.dryRun}) :\n- ${result.message}\n- Fichiers touches : ${(result.touchedFiles || []).join(", ") || "aucun"}\n- Resultats detailles :\n${(result.results || []).map((r) => ` - ${r.id || r.file || ""} ligne ${r.line} : ${r.status}${r.status === "mismatch" ? ` (attendu: ${r.expected?.slice(0, 40)}..., trouve: ${r.got?.slice(0, 40)}...)` : ""}`).join("\n")}\n${result.verification ? `Verification post-apply :\n${result.verification.map((v) => ` - ${v.name} : ${v.ok ? "OK" : "KO"}\n${v.output.slice(-200)}`).join("\n")}` : ""}`;
2234
+ return { query: "apply tasks", projectName: await getProjectName(absProject), applyReport: report };
2235
+ }
2236
+ );
2237
+
2238
+ registerTool(
2239
+ tools,
2240
+ "codebase_build",
2241
+ "Run and benchmark the project build (npm run build). Returns build time, output size, and files. Use when the user wants build performance, bundle size, or build issues.",
2242
+ {
2243
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2244
+ focus: { type: "string", description: "Optional focus." }
2245
+ },
2246
+ (value) => buildBuildPrompt(value.context, value.projectName, value.query),
2247
+ async (args, exec) => {
2248
+ const absProject = await resolveProjectPath(args.projectPath);
2249
+ const projectName = await getProjectName(absProject);
2250
+ const benchmark = await collectBuildBenchmark(absProject);
2251
+ const context = `=== BUILD BENCHMARK — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}\n\n${benchmark.ran ? `Durée: ${benchmark.duration}ms\nOK: ${benchmark.ok}\nDossier: ${benchmark.distInfo.path || "n/a"}\nTaille: ${benchmark.distInfo.size} octets\nFichiers:\n${benchmark.distInfo.files.map((f) => `- ${f.rel} (${f.size} o)`).join("\n")}\n\nLogs:\n${benchmark.summary}` : benchmark.reason}\n`;
2252
+ return { query: args.focus || "build benchmark", projectName, context };
2253
+ }
2254
+ );
2255
+
2256
+ registerTool(
2257
+ tools,
2258
+ "codebase_git",
2259
+ "Analyze the git history of a project: recent commits, diff stats, and working tree. Use when the user asks about recent changes, changelog, or git activity.",
2260
+ {
2261
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2262
+ focus: { type: "string", description: "Optional focus." }
2263
+ },
2264
+ (value) => buildGitPrompt(value.context, value.projectName, value.query),
2265
+ async (args, exec) => {
2266
+ const absProject = await resolveProjectPath(args.projectPath);
2267
+ const projectName = await getProjectName(absProject);
2268
+ const git = await collectGitSummary(absProject);
2269
+ const context = `=== GIT INTELLIGENCE — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}\n\n== DERNIER COMMITS ==\n${git.log}\n\n== DIFF STAT ==\n${git.diff}\n\n== WORKING TREE ==\n${git.status || "propre"}\n`;
2270
+ return { query: args.focus || "git summary", projectName, context };
2271
+ }
2272
+ );
2273
+
2274
+ registerTool(
2275
+ tools,
2276
+ "codebase_apply",
2277
+ "Apply a code patch to a file in the project. Provide the relative file path and the new file content. The tool checks the file stays within the project and is not protected, then writes it. Use with caution.",
2278
+ {
2279
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2280
+ filePath: { type: "string", description: "Relative path to the file to update." },
2281
+ newContent: { type: "string", description: "Complete new content for the file." }
2282
+ },
2283
+ (value) => buildApplyPrompt(value.filePath, value.newContent, value.projectName),
2284
+ async (args, exec) => {
2285
+ const absProject = await resolveProjectPath(args.projectPath);
2286
+ const projectName = await getProjectName(absProject);
2287
+ const result = await applyFilePatch(absProject, args.filePath, args.newContent);
2288
+ return { query: `apply ${args.filePath}`, projectName, context: `Fichier ${result.file} mis à jour avec succès.`, file: result.file };
2289
+ }
2290
+ );
2291
+
2292
+ registerTool(
2293
+ tools,
2294
+ "codebase_player",
2295
+ "Run a Player / UX playthrough brief on a codebase. Analyses the user journey, onboarding, friction, wow moments, and gamification opportunities. Use when the user wants a user-centric review, player perspective, UX walkthrough, or playtest of the app.",
2296
+ {
2297
+ projectPath: { type: "string", description: "Absolute local path to the project folder. If omitted, the project root is auto-detected from the current working directory." },
2298
+ focus: { type: "string", description: "Optional focus for the player brief (e.g. onboarding, checkout, first-run)." }
2299
+ },
2300
+ (value) => buildPlayerPrompt(value.context, value.projectName, value.query),
2301
+ async (args, exec) => {
2302
+ const absProject = await resolveProjectPath(args.projectPath);
2303
+ const projectName = await getProjectName(absProject);
2304
+ const { context } = await collectCodebaseContext(absProject, { focus: args.focus || "user journey and playthrough" });
2305
+ return { query: args.focus || "player brief", projectName, context };
2306
+ }
2307
+ );
2308
+
2309
+ async function runCommand(invocation, buildPromptFn, requireQuery = true) {
2310
+ const { rawInput, agent, signal } = invocation;
2311
+ let { projectPath, filePath, query, crea, creaTheme } = parseCodebaseInput(rawInput.trim());
2312
+ if (requireQuery && !query && !projectPath) return { kind: "error", text: "Fournis une question/terme/cible ou un chemin de projet." };
2313
+
2314
+ if (!query && projectPath) {
2315
+ query = "présente-moi ce projet de manière concise";
2316
+ }
2317
+
2318
+ const { absProject, context } = await collectCodebaseContext(projectPath, { focus: query, filePath, searchQuery: query });
2319
+ const projectName = await getProjectName(absProject);
2320
+ const prompt = buildPromptFn(query, context, projectName, crea, creaTheme);
2321
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2322
+ return { kind: "success", text: "Analyse codebase lancee." };
2323
+ }
2324
+
2325
+ commands.register({
2326
+ name: "codebase",
2327
+ description: "Poser une question sur le code source d'un projet. Le chemin peut etre passe directement : /codebase \"<chemin>\" [question]. /codebase <question> --project <chemin> [--file <fichier>] [--crea] [--crea-theme <theme>]",
2328
+ input: { hint: "question, --project <chemin>, --file <fichier>, --crea, --crea-theme <theme>" },
2329
+ async handler(invocation) {
2330
+ return runCommand(invocation, buildChatPrompt, true);
2331
+ }
2332
+ });
2333
+
2334
+ commands.register({
2335
+ name: "codebase-search",
2336
+ description: "Rechercher un terme dans le codebase. /codebase-search <terme> --project <chemin> [--crea] [--crea-theme <theme>]",
2337
+ input: { hint: "terme, --project <chemin>, --crea, --crea-theme <theme>" },
2338
+ async handler(invocation) {
2339
+ return runCommand(invocation, buildSearchPrompt, true);
2340
+ }
2341
+ });
2342
+
2343
+ commands.register({
2344
+ name: "codebase-explain",
2345
+ description: "Expliquer un fichier ou symbole. /codebase-explain <cible> --project <chemin> [--crea] [--crea-theme <theme>]",
2346
+ input: { hint: "cible, --project <chemin>, --crea, --crea-theme <theme>" },
2347
+ async handler(invocation) {
2348
+ return runCommand(invocation, buildExplainPrompt, true);
2349
+ }
2350
+ });
2351
+
2352
+ commands.register({
2353
+ name: "codebase-refactor",
2354
+ description: "Refactoriser un fichier. /codebase-refactor '<description>' --file <fichier> --project <chemin> [--crea] [--crea-theme <theme>]",
2355
+ input: { hint: "description, --file <fichier>, --project <chemin>, --crea, --crea-theme <theme>" },
2356
+ async handler(invocation) {
2357
+ const { rawInput, agent, signal } = invocation;
2358
+ const { projectPath, filePath, query, crea, creaTheme } = parseCodebaseInput(rawInput.trim());
2359
+ if (!query) return { kind: "error", text: "Decris ce qu'il faut refactoriser." };
2360
+ if (!filePath) return { kind: "error", text: "Fournis le fichier avec --file <fichier>." };
2361
+
2362
+ const { absProject, context } = await collectCodebaseContext(projectPath, { focus: query, filePath });
2363
+ const projectName = await getProjectName(absProject);
2364
+ const prompt = buildRefactorPrompt(filePath, query, context, projectName, crea, creaTheme);
2365
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2366
+ return { kind: "success", text: "Refactor propose." };
2367
+ }
2368
+ });
2369
+
2370
+ commands.register({
2371
+ name: "codebase-crea",
2372
+ description: "Générer un truc créatif à partir du codebase. /codebase-crea --project <chemin> [theme]",
2373
+ input: { hint: "theme optionnel, --project <chemin>" },
2374
+ async handler(invocation) {
2375
+ const { rawInput, agent, signal } = invocation;
2376
+ const { projectPath, query } = parseCodebaseInput(rawInput.trim());
2377
+
2378
+ const { absProject, context } = await collectCodebaseContext(projectPath, { focus: query || "creative output" });
2379
+ const projectName = await getProjectName(absProject);
2380
+ const prompt = buildCreaPrompt(query, context, projectName);
2381
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2382
+ return { kind: "success", text: "Créa lancee." };
2383
+ }
2384
+ });
2385
+
2386
+ commands.register({
2387
+ name: "codebase-intel",
2388
+ description: "Brief d'Intelligence Pro : architecture, graphe de modules, data flow, risques, opportunités et créa. /codebase-intel --project <chemin> [focus] [--style <ouf|punchy|dense|pedagogique|minimal>]",
2389
+ input: { hint: "focus optionnel, --project <chemin>, --style <ouf|punchy|dense|pedagogique|minimal>" },
2390
+ async handler(invocation) {
2391
+ const { rawInput, agent, signal } = invocation;
2392
+ const { projectPath, query, style } = parseCodebaseInput(rawInput.trim());
2393
+
2394
+ const { absProject, context } = await collectIntelligenceContext(projectPath, query || "professional intelligence brief");
2395
+ const projectName = await getProjectName(absProject);
2396
+ const prompt = buildIntelligencePrompt(context, projectName, query, style);
2397
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2398
+ return { kind: "success", text: "Brief d'intelligence codebase lance." };
2399
+ }
2400
+ });
2401
+
2402
+ commands.register({
2403
+ name: "codebase-audit",
2404
+ description: "Auditer les non-conformites, dette technique et erreurs. /codebase-audit --project <chemin> [focus]",
2405
+ input: { hint: "focus optionnel (security, types, console), --project <chemin>" },
2406
+ async handler(invocation) {
2407
+ const { rawInput, agent, signal } = invocation;
2408
+ const { projectPath, query } = parseCodebaseInput(rawInput.trim());
2409
+
2410
+ const { context, projectName } = await collectNonConformities(projectPath, query || "audit non-conformites");
2411
+ const prompt = buildAuditPrompt(context, projectName, query);
2412
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2413
+ return { kind: "success", text: "Audit non-conformites lance." };
2414
+ }
2415
+ });
2416
+
2417
+ commands.register({
2418
+ name: "codebase-report",
2419
+ description: "Rapport strategique et constat professionnel. /codebase-report --project <chemin> [focus] [--style <ouf|punchy|dense|pedagogique|minimal>]",
2420
+ input: { hint: "focus optionnel, --project <chemin>, --style <ouf|punchy|dense|pedagogique|minimal>" },
2421
+ async handler(invocation) {
2422
+ const { rawInput, agent, signal } = invocation;
2423
+ const { projectPath, query, style } = parseCodebaseInput(rawInput.trim());
2424
+
2425
+ const { context, projectName } = await collectAssessmentContext(projectPath, query || "rapport strategique");
2426
+ const prompt = buildReportPrompt(context, projectName, query, style);
2427
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2428
+ return { kind: "success", text: "Rapport strategique lance." };
2429
+ }
2430
+ });
2431
+
2432
+ commands.register({
2433
+ name: "codebase-tasks",
2434
+ description: "Générer un plan d'action TASKS.md. /codebase-tasks --project <chemin> [focus] [--style <ouf|punchy|dense|pedagogique|minimal>] [--raw]",
2435
+ input: { hint: "focus optionnel, --project <chemin>, --style <ouf|punchy|dense|pedagogique|minimal>" },
2436
+ async handler(invocation) {
2437
+ const { rawInput, agent, signal } = invocation;
2438
+ const { projectPath, query, style, raw, apply, dryRun } = parseCodebaseInput(rawInput.trim());
2439
+
2440
+ const { absProject, context, projectName, preTasks, metrics, constraints } = await collectTasksContext(projectPath, query || "plan d'action");
2441
+ if (raw) {
2442
+ if (isProtectedPath(absProject)) return { kind: "error", text: "Ce chemin est protege. TASKS.md n'a pas ete ecrit." };
2443
+ const markdown = formatRawTasksMarkdown(projectName, metrics, constraints, preTasks);
2444
+ const tasksPath = join(absProject, "TASKS.md");
2445
+ if (apply) {
2446
+ const result = await runApplyTasks(absProject, markdown, dryRun);
2447
+ const report = `Application des taches (dryRun=${dryRun}) :\n- ${result.message}\n- Fichiers touches : ${(result.touchedFiles || []).join(", ") || "aucun"}\n- Resultats detailles :\n${(result.results || []).map((r) => ` - ${r.id || r.file || ""} ligne ${r.line} : ${r.status}${r.status === "mismatch" ? ` (attendu: ${r.expected?.slice(0, 40)}..., trouve: ${r.got?.slice(0, 40)}...)` : ""}`).join("\n")}\n${result.verification ? `Verification post-apply :\n${result.verification.map((v) => ` - ${v.name} : ${v.ok ? "OK" : "KO"}\n${v.output.slice(-200)}`).join("\n")}` : ""}`;
2448
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, report, signal);
2449
+ return { kind: "success", text: dryRun ? "Dry-run des taches termine." : "Application des taches terminee." };
2450
+ }
2451
+ await writeFile(tasksPath, markdown, "utf8");
2452
+ return { kind: "success", text: `TASKS.md brut genere : ${tasksPath}\n\n${markdown.slice(0, 2000)}\n\n[... tronque si necessaire ...]` };
2453
+ }
2454
+ const prompt = buildTasksPrompt(context, projectName, query, style);
2455
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2456
+ return { kind: "success", text: "Plan d'action TASKS lance." };
2457
+ }
2458
+ });
2459
+
2460
+ commands.register({
2461
+ name: "codebase-apply-tasks",
2462
+ description: "Appliquer le plan TASKS.md au projet. /codebase-apply-tasks --project <chemin> [--file <TASKS.md>] [--dry-run]",
2463
+ input: { hint: "--project <chemin>, --file <TASKS.md>, --dry-run" },
2464
+ async handler(invocation) {
2465
+ const { rawInput, agent, signal } = invocation;
2466
+ const { projectPath, filePath, dryRun } = parseCodebaseInput(rawInput.trim());
2467
+
2468
+ const absProject = await resolveProjectPath(projectPath);
2469
+ const tasksPath = filePath ? resolve(join(absProject, filePath)) : join(absProject, "TASKS.md");
2470
+ const markdown = await safeReadText(tasksPath);
2471
+ if (!markdown) return { kind: "error", text: `TASKS.md introuvable : ${tasksPath}` };
2472
+ const result = await runApplyTasks(absProject, markdown, dryRun);
2473
+ const report = `Application des taches (dryRun=${dryRun}) :\n- ${result.message}\n- Fichiers touches : ${(result.touchedFiles || []).join(", ") || "aucun"}\n- Resultats detailles :\n${(result.results || []).map((r) => ` - ${r.id || r.file || ""} ligne ${r.line} : ${r.status}${r.status === "mismatch" ? ` (attendu: ${r.expected?.slice(0, 40)}..., trouve: ${r.got?.slice(0, 40)}...)` : ""}`).join("\n")}\n${result.verification ? `Verification post-apply :\n${result.verification.map((v) => ` - ${v.name} : ${v.ok ? "OK" : "KO"}\n${v.output.slice(-200)}`).join("\n")}` : ""}`;
2474
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, report, signal);
2475
+ return { kind: "success", text: dryRun ? "Dry-run des taches termine." : "Application des taches terminee." };
2476
+ }
2477
+ });
2478
+
2479
+ commands.register({
2480
+ name: "codebase-ceo",
2481
+ description: "One-Page CEO Brief ultra-percutant. /codebase-ceo --project <chemin> [focus]",
2482
+ input: { hint: "focus optionnel, --project <chemin>" },
2483
+ async handler(invocation) {
2484
+ const { rawInput, agent, signal } = invocation;
2485
+ const { projectPath, query } = parseCodebaseInput(rawInput.trim());
2486
+
2487
+ const { context, projectName } = await collectCeoContext(projectPath, query || "one page ceo brief");
2488
+ const prompt = buildCeoPrompt(context, projectName, query);
2489
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2490
+ return { kind: "success", text: "One-Page CEO Brief lance." };
2491
+ }
2492
+ });
2493
+
2494
+ commands.register({
2495
+ name: "codebase-build",
2496
+ description: "Benchmark du build. /codebase-build --project <chemin>",
2497
+ input: { hint: "--project <chemin>" },
2498
+ async handler(invocation) {
2499
+ const { rawInput, agent, signal } = invocation;
2500
+ const { projectPath } = parseCodebaseInput(rawInput.trim());
2501
+
2502
+ const absProject = await resolveProjectPath(projectPath);
2503
+ const projectName = await getProjectName(absProject);
2504
+ const benchmark = await collectBuildBenchmark(absProject);
2505
+ const context = `=== BUILD BENCHMARK — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}\n\n${benchmark.ran ? `Durée: ${benchmark.duration}ms\nOK: ${benchmark.ok}\nDossier: ${benchmark.distInfo.path || "n/a"}\nTaille: ${benchmark.distInfo.size} octets\nFichiers:\n${benchmark.distInfo.files.map((f) => `- ${f.rel} (${f.size} o)`).join("\n")}\n\nLogs:\n${benchmark.summary}` : benchmark.reason}\n`;
2506
+ const prompt = buildBuildPrompt(context, projectName, "");
2507
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2508
+ return { kind: "success", text: "Benchmark build lance." };
2509
+ }
2510
+ });
2511
+
2512
+ commands.register({
2513
+ name: "codebase-git",
2514
+ description: "Analyse git (commits, diff, working tree). /codebase-git --project <chemin>",
2515
+ input: { hint: "--project <chemin>" },
2516
+ async handler(invocation) {
2517
+ const { rawInput, agent, signal } = invocation;
2518
+ const { projectPath } = parseCodebaseInput(rawInput.trim());
2519
+
2520
+ const absProject = await resolveProjectPath(projectPath);
2521
+ const projectName = await getProjectName(absProject);
2522
+ const git = await collectGitSummary(absProject);
2523
+ const context = `=== GIT INTELLIGENCE — ${(projectName ?? "").toUpperCase()} ===\nProject: ${absProject}\n\n== DERNIER COMMITS ==\n${git.log}\n\n== DIFF STAT ==\n${git.diff}\n\n== WORKING TREE ==\n${git.status || "propre"}\n`;
2524
+ const prompt = buildGitPrompt(context, projectName, "");
2525
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2526
+ return { kind: "success", text: "Analyse git lancee." };
2527
+ }
2528
+ });
2529
+
2530
+ commands.register({
2531
+ name: "codebase-apply",
2532
+ description: "Appliquer un patch a un fichier. /codebase-apply <fichier> --content '<contenu>' --project <chemin>",
2533
+ input: { hint: "fichier, --content <contenu>, --project <chemin>" },
2534
+ async handler(invocation) {
2535
+ const { rawInput, agent, signal } = invocation;
2536
+ const { projectPath, filePath, query } = parseCodebaseInput(rawInput.trim());
2537
+
2538
+ if (!filePath) return { kind: "error", text: "Fournis le fichier avec --file <fichier>." };
2539
+ if (!query) return { kind: "error", text: "Fournis le nouveau contenu avec --content '<contenu>'." };
2540
+ const absProject = await resolveProjectPath(projectPath);
2541
+ const projectName = await getProjectName(absProject);
2542
+ const result = await applyFilePatch(absProject, filePath, query);
2543
+ const prompt = buildApplyPrompt(result.file, query, projectName);
2544
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2545
+ return { kind: "success", text: `Fichier ${result.file} mis a jour.` };
2546
+ }
2547
+ });
2548
+
2549
+ commands.register({
2550
+ name: "codebase-player",
2551
+ description: "Player / UX playthrough brief. /codebase-player --project <chemin> [focus]",
2552
+ input: { hint: "focus optionnel (onboarding, checkout...), --project <chemin>" },
2553
+ async handler(invocation) {
2554
+ const { rawInput, agent, signal } = invocation;
2555
+ const { projectPath, query } = parseCodebaseInput(rawInput.trim());
2556
+
2557
+ const absProject = await resolveProjectPath(projectPath);
2558
+ const projectName = await getProjectName(absProject);
2559
+ const { context } = await collectCodebaseContext(absProject, { focus: query || "user journey and playthrough" });
2560
+ const prompt = buildPlayerPrompt(context, projectName, query);
2561
+ await submitToAgent(ctx, agent?.sessionId ?? agent?.id, prompt, signal);
2562
+ return { kind: "success", text: "Player brief lance." };
2563
+ }
2564
+ });
2565
+
2566
+ systemPrompt?.section?.({
2567
+ name: "codebase-chat",
2568
+ order: 130,
2569
+ text: `You have access to a powerful codebase plugin (dsh-codebase-chat v${VERSION}). When the user asks about code, files, project structure, architecture, how something works, where something is, or wants to search/explain/refactor/audit/report on code, use the appropriate tool: codebase_chat, codebase_search, codebase_explain, codebase_refactor, codebase_crea, codebase_intelligence, codebase_audit, codebase_report, codebase_ceo, codebase_tasks, codebase_build, codebase_git, codebase_apply, or codebase_player. codebase_intelligence is the premium auto-mode: it builds a structured context and asks for a professional, stylish intelligence brief. codebase_ceo is the one-page executive brief: metrics, score cards, top risks, top opportunities, SWOT, killer move, perfect for a board or investor. codebase_audit is the dedicated non-conformity and tech-debt auditor. codebase_report is the executive strategic report: a beautiful, visual, deep assessment (SWOT, score cards, 90-day roadmap, marketing) perfect for boards or clients. codebase_player is the UX / playthrough / player perspective brief: user journey, onboarding, friction, wow moments, gamification, and score cards from a user's point of view.\n\nIf the user asks for raw tasks, a TASKS.md with Avant/Après code blocks, or uses --raw, call codebase_tasks with raw=true. It writes TASKS.md directly in the project and returns the path/content. If the user asks to apply the plan, run the fixes, or says 'apply tasks', call codebase_apply_tasks. Use dryRun=true only if the user asks for a simulation or dry-run. If the user confirms or says 'apply', 'execute', 'yes', or 'oui', call with dryRun=false and apply the patches.\n\nIf no project path is given, first attempt to auto-detect the project root from the current working directory (cwd). Auto-detection walks up the tree looking for package.json, AGENTS.md, .git or tsconfig.json. Do NOT fall back to any known project (including the 'Dako' alias) unless the user explicitly mentions it. The only recognized alias is 'Dako' for the path 'D:\\Nouveau dossier'; use it only when the user explicitly says 'Dako' or uses --project Dako. If auto-detection fails and no explicit path is provided, ask the user for the path. Always answer in the same language as the user's message. If the user wants a creative idea at the end, set crea=true or use codebase_crea. Users may also use slash commands /codebase, /codebase-search, /codebase-explain, /codebase-refactor, /codebase-crea, /codebase-intel, /codebase-audit, /codebase-report, /codebase-ceo, /codebase-tasks, /codebase-tasks-raw, /codebase-apply-tasks, /codebase-build, /codebase-git, /codebase-apply, /codebase-player.`
2570
+ });
2571
+
2572
+ function sameOrigin(req) {
2573
+ const host = req.headers.host;
2574
+ if (!host) return false;
2575
+ let candidate = req.headers.origin;
2576
+ if (!candidate) {
2577
+ candidate = req.headers.referer;
2578
+ }
2579
+ if (!candidate) {
2580
+ const site = req.headers["sec-fetch-site"];
2581
+ if (site === "same-origin") return true;
2582
+ return false;
2583
+ }
2584
+ try {
2585
+ const url = new URL(candidate);
2586
+ return url.host === host && (url.protocol === "http:" || url.protocol === "https:");
2587
+ } catch {
2588
+ return false;
2589
+ }
2590
+ }
2591
+
2592
+ async function readJson(req, limit = 64 * 1024 * 1024) {
2593
+ const chunks = [];
2594
+ let size = 0;
2595
+ for await (const value of req) {
2596
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
2597
+ size += chunk.byteLength;
2598
+ if (size > limit) throw new Error("Request body is too large.");
2599
+ chunks.push(chunk);
2600
+ }
2601
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2602
+ }
2603
+
2604
+ function writeJson(res, body, status = 200) {
2605
+ const text = JSON.stringify(body);
2606
+ res.writeHead(status, {
2607
+ "Content-Type": "application/json; charset=utf-8",
2608
+ "Content-Length": Buffer.byteLength(text),
2609
+ "X-Content-Type-Options": "nosniff"
2610
+ });
2611
+ res.end(text);
2612
+ }
2613
+
2614
+ function sessionExists(sessionId) {
2615
+ try {
2616
+ const sessions = ctx.get("sessions");
2617
+ if (sessions?.get(sessionId) !== void 0) return true;
2618
+ } catch { }
2619
+ try {
2620
+ const agents = ctx.get("agents");
2621
+ if (agents?.get(sessionId) !== void 0) return true;
2622
+ } catch { }
2623
+ return false;
2624
+ }
2625
+
2626
+ async function buildRoutePrompt(body) {
2627
+ setProgressSession(body.sessionId);
2628
+ reportProgress(`Préparation du brief ${body.mode || "codebase"}...`);
2629
+ const projectPath = body.projectPath || "";
2630
+ const query = typeof body.query === "string" ? body.query : "";
2631
+ const filePath = typeof body.filePath === "string" ? body.filePath : "";
2632
+ const rawMode = typeof body.mode === "string" ? body.mode.toLowerCase() : "chat";
2633
+ const mode = {
2634
+ rapport: "report",
2635
+ intelligence: "intel",
2636
+ inteligence: "intel",
2637
+ tache: "tasks",
2638
+ taches: "tasks",
2639
+ task: "tasks",
2640
+ "tasks-raw": "tasks-raw",
2641
+ playthrough: "player",
2642
+ ux: "player",
2643
+ executive: "ceo",
2644
+ board: "ceo"
2645
+ }[rawMode] || rawMode;
2646
+ const rawStyle = typeof body.style === "string" ? body.style.toLowerCase() : "ouf";
2647
+ const style = { wow: "ouf", ouf: "ouf", best: "ouf" }[rawStyle] || rawStyle;
2648
+ const crea = body.crea === true || body.crea === "true";
2649
+ const creaTheme = typeof body.creaTheme === "string" ? body.creaTheme : "";
2650
+ const lang = typeof body.lang === "string" ? body.lang.toLowerCase() : "fr";
2651
+ const langHint = lang === "en" ? "Respond strictly in English." : "Réponds obligatoirement en français.";
2652
+ const finalBlock = lang === "en"
2653
+ ? "\n\n---\n\nFINAL INSTRUCTION (overrides everything above): The entire response, including section titles, bullet points and conclusion, MUST be written in English. Do not output any French words except in quoted code or file paths."
2654
+ : "\n\n---\n\nINSTRUCTION FINALE (prime sur tout le reste): La reponse entiere, titres de sections inclus, DOIT etre en francais. Ne produis aucun mot anglais sauf dans du code ou des chemins de fichiers cites.";
2655
+ const wrap = (prompt) => {
2656
+ const p = normalizeLabels(prompt, lang);
2657
+ return `${langHint}\n\n${p}${finalBlock}`;
2658
+ };
2659
+
2660
+ if (mode === "intel") {
2661
+ const { absProject, context } = await collectIntelligenceContext(projectPath, query, lang);
2662
+ const projectName = await getProjectName(absProject);
2663
+ return wrap(buildIntelligencePrompt(context, projectName, query, style, lang));
2664
+ }
2665
+ if (mode === "report") {
2666
+ const { context, projectName } = await collectAssessmentContext(projectPath, query, lang);
2667
+ return wrap(buildReportPrompt(context, projectName, query, style, lang));
2668
+ }
2669
+ if (mode === "audit") {
2670
+ const { context, projectName } = await collectNonConformities(projectPath, query, lang);
2671
+ return wrap(buildAuditPrompt(context, projectName, query, lang));
2672
+ }
2673
+ if (mode === "tasks") {
2674
+ const { context, projectName } = await collectTasksContext(projectPath, query || "plan d'action", lang);
2675
+ return wrap(buildTasksPrompt(context, projectName, query, style, lang));
2676
+ }
2677
+ if (mode === "tasks-raw") {
2678
+ const { projectName, preTasks, metrics, constraints } = await collectTasksContext(projectPath, query || "plan d'action", lang);
2679
+ return wrap(formatRawTasksMarkdown(projectName, metrics, constraints, preTasks, lang));
2680
+ }
2681
+ if (mode === "ceo") {
2682
+ const { context, projectName } = await collectCeoContext(projectPath, query || "one page executive brief", lang);
2683
+ return wrap(buildCeoPrompt(context, projectName, query, lang));
2684
+ }
2685
+ if (mode === "player") {
2686
+ const absProject = await resolveProjectPath(projectPath);
2687
+ const projectName = await getProjectName(absProject);
2688
+ const { context } = await collectCodebaseContext(absProject, { focus: query || "user journey and playthrough", lang });
2689
+ return wrap(buildPlayerPrompt(context, projectName, query, lang));
2690
+ }
2691
+ const { absProject, context } = await collectCodebaseContext(projectPath, {
2692
+ focus: query,
2693
+ filePath: mode === "explain" || mode === "refactor" ? (query || filePath) : filePath,
2694
+ searchQuery: mode === "search" ? query : "",
2695
+ lang
2696
+ });
2697
+ const projectName = await getProjectName(absProject);
2698
+
2699
+ if (mode === "crea") return wrap(buildCreaPrompt(query, context, projectName, lang));
2700
+ if (mode === "search") return wrap(buildSearchPrompt(query, context, projectName, crea, creaTheme, lang));
2701
+ if (mode === "explain") return wrap(buildExplainPrompt(query || filePath, context, projectName, crea, creaTheme, lang));
2702
+ if (mode === "refactor") return wrap(buildRefactorPrompt(filePath || query, query, context, projectName, crea, creaTheme, lang));
2703
+ reportProgress("Brief prêt, envoi à l'agent...");
2704
+ return wrap(buildChatPrompt(query, context, projectName, crea, creaTheme, lang));
2705
+ }
2706
+
2707
+ async function buildPreview(projectPath) {
2708
+ const absProject = await resolveProjectPath(projectPath);
2709
+ const projectName = await getProjectName(absProject);
2710
+ const summary = await getPackageSummary(absProject);
2711
+ const metrics = await collectMetrics(absProject);
2712
+ return {
2713
+ name: projectName,
2714
+ path: absProject,
2715
+ summary,
2716
+ files: metrics.files,
2717
+ sourceLines: metrics.sourceLines,
2718
+ testFiles: metrics.testFiles,
2719
+ languages: metrics.languages
2720
+ };
2721
+ }
2722
+
2723
+ function registerRoutes(webServer) {
2724
+ webServer.register({
2725
+ kind: "exact",
2726
+ path: "/codebase-chat/progress",
2727
+ async handler(req, res) {
2728
+ try {
2729
+ if (req.method !== "GET") {
2730
+ writeJson(res, { ok: false, error: "Method not allowed" }, 405);
2731
+ return;
2732
+ }
2733
+ if (!sameOrigin(req)) {
2734
+ writeJson(res, { ok: false, error: "Same-origin request required" }, 403);
2735
+ return;
2736
+ }
2737
+ const url = new URL(req.url, `http://${req.headers.host}`);
2738
+ const sessionId = url.searchParams.get("sessionId");
2739
+ if (!sessionId || !sessionExists(sessionId)) {
2740
+ writeJson(res, { ok: false, error: "Session not found" }, 404);
2741
+ return;
2742
+ }
2743
+ const messages = (progressStore.get(sessionId) || []).map((m) => m.message);
2744
+ writeJson(res, { ok: true, messages });
2745
+ } catch (error) {
2746
+ const message = error instanceof Error ? error.message : String(error);
2747
+ writeJson(res, { ok: false, error: message }, 500);
2748
+ }
2749
+ }
2750
+ });
2751
+ webServer.register({
2752
+ kind: "exact",
2753
+ path: "/codebase-chat/ask",
2754
+ async handler(req, res) {
2755
+ try {
2756
+ if (req.method !== "POST") {
2757
+ writeJson(res, { ok: false, error: "Method not allowed" }, 405);
2758
+ return;
2759
+ }
2760
+ if (!sameOrigin(req)) {
2761
+ writeJson(res, { ok: false, error: "Same-origin request required" }, 403);
2762
+ return;
2763
+ }
2764
+ const body = await readJson(req);
2765
+ const sessionId = body.sessionId;
2766
+ if (!sessionId || !sessionExists(sessionId)) {
2767
+ writeJson(res, { ok: false, error: "Session not found" }, 404);
2768
+ return;
2769
+ }
2770
+ const prompt = await buildRoutePrompt(body);
2771
+ await submitToAgent(ctx, sessionId, prompt, void 0);
2772
+ writeJson(res, { ok: true });
2773
+ } catch (error) {
2774
+ const message = error instanceof Error ? error.message : String(error);
2775
+ if (res.headersSent) return;
2776
+ const known = ["Session not found", "Aucune session active", "Ce chemin est protege", "Le chemin n'est pas un dossier", "Chemin du projet manquant", "Aucun projet detecte"];
2777
+ const status = known.some((k) => message.includes(k)) ? 400 : 500;
2778
+ writeJson(res, { ok: false, error: message }, status);
2779
+ }
2780
+ }
2781
+ });
2782
+ webServer.register({
2783
+ kind: "exact",
2784
+ path: "/codebase-chat/preview",
2785
+ async handler(req, res) {
2786
+ try {
2787
+ if (req.method !== "POST") {
2788
+ writeJson(res, { ok: false, error: "Method not allowed" }, 405);
2789
+ return;
2790
+ }
2791
+ if (!sameOrigin(req)) {
2792
+ writeJson(res, { ok: false, error: "Same-origin request required" }, 403);
2793
+ return;
2794
+ }
2795
+ const body = await readJson(req);
2796
+ if (!body.projectPath) {
2797
+ writeJson(res, { ok: true, empty: true });
2798
+ return;
2799
+ }
2800
+ const preview = await buildPreview(body.projectPath);
2801
+ writeJson(res, { ok: true, ...preview });
2802
+ } catch (error) {
2803
+ const message = error instanceof Error ? error.message : String(error);
2804
+ const known = ["Ce chemin est protege", "Le chemin n'est pas un dossier", "Aucun projet detecte"];
2805
+ const status = known.some((k) => message.includes(k)) ? 400 : 500;
2806
+ writeJson(res, { ok: false, error: message }, status);
2807
+ }
2808
+ }
2809
+ });
2810
+ }
2811
+
2812
+ const webServer = ctx.get("webServer");
2813
+ if (webServer !== void 0) {
2814
+ registerRoutes(webServer);
2815
+ } else {
2816
+ ctx.inject(["webServer"], (scoped) => registerRoutes(scoped.webServer));
2817
+ }
2818
+ }
2819
+
2820
+ export {
2821
+ apply,
2822
+ inject,
2823
+ name,
2824
+ normalizeLabels,
2825
+ resolveProjectPath,
2826
+ getProjectName,
2827
+ findProjectRoot,
2828
+ getPackageSummary,
2829
+ collectMetrics,
2830
+ collectCodebaseContext,
2831
+ collectIntelligenceContext,
2832
+ collectAssessmentContext,
2833
+ collectNonConformities,
2834
+ collectCeoContext,
2835
+ collectTasksContext,
2836
+ buildChatPrompt,
2837
+ buildSearchPrompt,
2838
+ buildExplainPrompt,
2839
+ buildRefactorPrompt,
2840
+ buildCreaPrompt,
2841
+ buildIntelligencePrompt,
2842
+ buildAuditPrompt,
2843
+ buildReportPrompt,
2844
+ buildTasksPrompt,
2845
+ buildCeoPrompt,
2846
+ buildBuildPrompt,
2847
+ buildPlayerPrompt,
2848
+ buildGitPrompt,
2849
+ buildApplyPrompt,
2850
+ formatRawTasksMarkdown,
2851
+ reportProgress,
2852
+ setProgressSession,
2853
+ langInstruction,
2854
+ styleInstruction,
2855
+ bannerInstruction,
2856
+ citationInstruction
2857
+ };