chore: deep audit, hardening, docs sync and workflow cleanup
This commit is contained in:
@@ -339,3 +339,54 @@ Reste à faire (futur) :
|
||||
- [ ] MCP (Model Context Protocol) pour intégration outils
|
||||
- [ ] WebRTC voice (streaming temps réel au lieu d'upload)
|
||||
- [ ] Fine-tune personas dédié (OpenCharacter/Ditto methodology)
|
||||
|
||||
## Lot 17 — Deep Audit & Refactoring `[en cours]`
|
||||
|
||||
Objectif : audit complet du code, optimisations chirurgicales, documentation enrichie.
|
||||
|
||||
### Phase A — Analyse & documentation `[en cours]`
|
||||
|
||||
- [x] Deep audit TUI script (ops/v2/deep-audit.js) — security, perf, complexity, deps
|
||||
- [x] Veille OSS enrichie (10 nouvelles categories: voice cloning, music gen, PDF, RAG, WebRTC, MCP, persona fine-tune)
|
||||
- [x] Diagrammes Mermaid ajoutés (Context Store, Docker deploy, Inter-persona dialogue)
|
||||
- [x] AGENTS.md refondu (matrice 10 agents, Mermaid skill routing, pipeline intervention)
|
||||
- [ ] Consolidation PLAN.md et TODO.md avec etat reel
|
||||
- [ ] Deep analyse code par agents (5 agents paralleles: api, web, packages, mascarade, v1+worker)
|
||||
|
||||
### Phase B — Refactoring code `[planifié]`
|
||||
|
||||
- [ ] ws-chat.ts: extraction modules (1449 LOC → 4 modules <400 LOC)
|
||||
- [ ] app.ts: extraction routes + middleware + handlers (1292 LOC → 3 modules)
|
||||
- [ ] writeFileSync → async dans ws-chat.ts (3 occurrences P2)
|
||||
- [ ] console.log → structured logging (apps/api, apps/worker)
|
||||
- [ ] React.memo + lazy load sur composants lourds (Chat, ChatHistory, VoiceChat, NodeEditor)
|
||||
|
||||
### Phase C — Infrastructure `[planifié]`
|
||||
|
||||
- [ ] Ajouter SearXNG au docker-compose (remplacer DuckDuckGo scraping)
|
||||
- [ ] Ajouter MinerU/Docling (remplacer pdf-parse)
|
||||
- [ ] Spike BGE-M3 embeddings (upgrade RAG)
|
||||
- [ ] Deployer deep-audit.js sur kxkm-ai
|
||||
|
||||
### Phase D — Nouveaux node types `[planifié]`
|
||||
|
||||
- [ ] Node type `music_generation` (ACE-Step 1.5, <4GB VRAM)
|
||||
- [ ] Node type `voice_clone` (XTTS-v2, zero-shot 6s reference)
|
||||
- [ ] Node type `document_extraction` (MinerU/Docling)
|
||||
|
||||
## Lot 18 — Voice & MCP `[futur]`
|
||||
|
||||
Objectif : voix temps réel et intégration outils standardisée.
|
||||
|
||||
- [ ] XTTS-v2 voice cloning par persona (remplacer Piper pour voix uniques)
|
||||
- [ ] LLMRTC WebRTC streaming (TypeScript SDK, VAD, barge-in)
|
||||
- [ ] MCP SDK integration (personas comme MCP servers, tool-calling)
|
||||
- [ ] PCL + OpenCharacter (pipeline fine-tune persona avancé)
|
||||
- [ ] Chatterbox TTS evaluation (remplacement Piper qualité)
|
||||
|
||||
## Lot 19 — Music & Creative `[futur]`
|
||||
|
||||
- [ ] ACE-Step 1.5 production (génération musicale)
|
||||
- [ ] `/compose` command (prompt → musique via Node Engine)
|
||||
- [ ] Flux 2 dans ComfyUI (upgrade image gen)
|
||||
- [ ] A2A Protocol evaluation (interop agents externes)
|
||||
|
||||
@@ -135,3 +135,61 @@
|
||||
- [x] Docker — `Dockerfile` (multi-stage Node 22 alpine) + `docker-compose.yml` (5 services) + `.dockerignore`
|
||||
- [ ] Documentation utilisateur
|
||||
- [ ] Performance profiling
|
||||
|
||||
## P11 Lot 17 — Deep Audit & Refactoring
|
||||
|
||||
### Phase A — Analyse & documentation
|
||||
|
||||
- [x] Script TUI deep-audit.js (security, perf, complexity, deps) — `ops/v2/deep-audit.js`
|
||||
- [x] Veille OSS enrichie 2026-03-17 (10 nouvelles catégories) — `docs/OSS_WATCH_2026-03-16.md`
|
||||
- [x] Diagrammes Mermaid (Context Store, Docker, Inter-persona) — `docs/ARCHITECTURE.md`
|
||||
- [x] AGENTS.md refondu (matrice 10 agents, Mermaid routing, pipeline) — `docs/AGENTS.md`
|
||||
- [x] PLAN.md consolidé avec lots 17-19
|
||||
- [x] TODO.md consolidé avec backlog Phase 6+
|
||||
- [ ] Deep analyse code agents (api, web, packages, mascarade, v1+worker) — en cours
|
||||
|
||||
### Phase B — Refactoring code
|
||||
|
||||
- [ ] **P1** ws-chat.ts: extraction modules (1449 LOC → ~4×350 LOC)
|
||||
- [ ] Extraire `ws-multimodal.ts` (vision, STT, TTS, PDF handlers)
|
||||
- [ ] Extraire `ws-persona-router.ts` (pickResponders, inter-persona, memory)
|
||||
- [ ] Extraire `ws-commands.ts` (slash commands, /web, /imagine)
|
||||
- [ ] Garder `ws-chat.ts` core (WebSocket lifecycle, broadcast, rate limit)
|
||||
- [ ] **P1** app.ts: extraction routes (1292 LOC → routes/ + middleware/)
|
||||
- [ ] Extraire `routes/personas.ts`
|
||||
- [ ] Extraire `routes/node-engine.ts`
|
||||
- [ ] Extraire `routes/chat.ts`
|
||||
- [ ] Extraire `middleware/auth.ts`
|
||||
- [ ] **P2** writeFileSync → appendFile async dans ws-chat.ts (3 occurrences)
|
||||
- [ ] **P2** console.log → logger structuré (apps/api, apps/worker)
|
||||
- [ ] **P2** React.memo sur Chat, ChatHistory, VoiceChat, NodeEditor
|
||||
- [ ] **P2** Lazy load: React.lazy + Suspense pour routes lourdes
|
||||
|
||||
### Phase C — Infrastructure
|
||||
|
||||
- [ ] SearXNG dans docker-compose (service searxng:8080, remplacer DuckDuckGo)
|
||||
- [ ] MinerU/Docling dans docker-compose (remplacer pdf-parse)
|
||||
- [ ] Spike BGE-M3 embeddings (upgrade nomic-embed-text)
|
||||
- [ ] Déployer deep-audit.js sur kxkm-ai (cron quotidien)
|
||||
- [ ] Créer utilisateur Discord **Pharmacius** (bot orchestrateur, bridge chat Discord ↔ KXKM)
|
||||
|
||||
### Phase D — Nouveaux node types
|
||||
|
||||
- [ ] `music_generation` node (ACE-Step 1.5, <4GB VRAM)
|
||||
- [ ] `voice_clone` node (XTTS-v2, zero-shot 6s reference)
|
||||
- [ ] `document_extraction` node (MinerU/Docling)
|
||||
|
||||
## P12 Lot 18 — Voice & MCP (futur)
|
||||
|
||||
- [ ] XTTS-v2 voice cloning par persona
|
||||
- [ ] LLMRTC WebRTC streaming (TypeScript, VAD, barge-in)
|
||||
- [ ] MCP SDK integration (personas = MCP servers)
|
||||
- [ ] PCL + OpenCharacter pipeline fine-tune
|
||||
- [ ] Chatterbox TTS evaluation
|
||||
|
||||
## P13 Lot 19 — Music & Creative (futur)
|
||||
|
||||
- [ ] ACE-Step 1.5 production
|
||||
- [ ] `/compose` command (prompt → musique)
|
||||
- [ ] Flux 2 dans ComfyUI
|
||||
- [ ] A2A Protocol evaluation
|
||||
|
||||
+27
-8
@@ -866,8 +866,17 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
|
||||
const voiceSamplesDir = path.resolve(process.cwd(), "data", "voice-samples");
|
||||
await mkdir(voiceSamplesDir, { recursive: true });
|
||||
|
||||
const sampleName = persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
||||
const sampleName = path.basename(persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
|
||||
if (!sampleName || sampleName === "." || sampleName === "..") {
|
||||
res.status(400).json({ ok: false, error: "invalid_persona_name" });
|
||||
return;
|
||||
}
|
||||
const samplePath = path.join(voiceSamplesDir, `${sampleName}.wav`);
|
||||
// Boundary check: ensure resolved path stays within voiceSamplesDir
|
||||
if (!path.resolve(samplePath).startsWith(voiceSamplesDir)) {
|
||||
res.status(400).json({ ok: false, error: "path_traversal_blocked" });
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile(samplePath, buffer);
|
||||
|
||||
@@ -882,8 +891,13 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleName = persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
||||
const samplePath = path.resolve(process.cwd(), "data", "voice-samples", `${sampleName}.wav`);
|
||||
const voiceSamplesDir2 = path.resolve(process.cwd(), "data", "voice-samples");
|
||||
const sampleName = path.basename(persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
|
||||
const samplePath = path.join(voiceSamplesDir2, `${sampleName}.wav`);
|
||||
if (!sampleName || !path.resolve(samplePath).startsWith(voiceSamplesDir2)) {
|
||||
res.status(400).json({ ok: false, error: "invalid_persona_name" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { unlink } = await import("node:fs/promises");
|
||||
@@ -902,12 +916,17 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleName = persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
||||
const samplePath = path.resolve(process.cwd(), "data", "voice-samples", `${sampleName}.wav`);
|
||||
const voiceSamplesDir3 = path.resolve(process.cwd(), "data", "voice-samples");
|
||||
const sampleName2 = path.basename(persona.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
|
||||
const samplePath2 = path.join(voiceSamplesDir3, `${sampleName2}.wav`);
|
||||
if (!sampleName2 || !path.resolve(samplePath2).startsWith(voiceSamplesDir3)) {
|
||||
res.json({ ok: true, data: { hasVoiceSample: false } });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await stat(samplePath);
|
||||
res.json({ ok: true, data: { hasVoiceSample: true, samplePath: `data/voice-samples/${sampleName}.wav` } });
|
||||
await stat(samplePath2);
|
||||
res.json({ ok: true, data: { hasVoiceSample: true, samplePath: `data/voice-samples/${sampleName2}.wav` } });
|
||||
} catch {
|
||||
res.json({ ok: true, data: { hasVoiceSample: false } });
|
||||
}
|
||||
@@ -1019,7 +1038,7 @@ export async function createApp(): Promise<{ app: express.Express; personaRepo:
|
||||
// Analytics — aggregate chat log stats
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
app.get("/api/v2/analytics", async (_req, res) => {
|
||||
app.get("/api/v2/analytics", requirePermission("ops:read"), async (_req: SessionRequest, res) => {
|
||||
const logDir = path.join(process.cwd(), process.env.KXKM_LOCAL_DATA_DIR || "data", "chat-logs");
|
||||
const stats = {
|
||||
totalMessages: 0,
|
||||
|
||||
@@ -76,6 +76,23 @@ export class ContextStore {
|
||||
return path.join(this.options.dataDir, `${safe}.summary.json`);
|
||||
}
|
||||
|
||||
private parseJson<T>(raw: string): T | null {
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async readSummary(channel: string): Promise<ContextSummary | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
|
||||
return this.parseJson<ContextSummary>(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
await fs.mkdir(this.options.dataDir, { recursive: true });
|
||||
@@ -109,14 +126,9 @@ export class ContextStore {
|
||||
|
||||
// 1. Load compacted summary
|
||||
let summary = "";
|
||||
try {
|
||||
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
|
||||
const data: ContextSummary = JSON.parse(raw);
|
||||
if (data.summaryText) {
|
||||
summary = `[Résumé des conversations précédentes]\n${data.summaryText}`;
|
||||
}
|
||||
} catch {
|
||||
// No summary yet
|
||||
const summaryData = await this.readSummary(channel);
|
||||
if (summaryData?.summaryText) {
|
||||
summary = `[Résumé des conversations précédentes]\n${summaryData.summaryText}`;
|
||||
}
|
||||
|
||||
// 2. Load recent raw entries
|
||||
@@ -197,13 +209,9 @@ export class ContextStore {
|
||||
.join("\n")
|
||||
.slice(0, 30000); // cap for LLM context
|
||||
|
||||
// Load existing summary
|
||||
let existingSummary = "";
|
||||
try {
|
||||
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
|
||||
const data: ContextSummary = JSON.parse(raw);
|
||||
existingSummary = data.summaryText || "";
|
||||
} catch { /* no previous summary */ }
|
||||
// Load existing summary once to avoid redundant reads/parses.
|
||||
const previousSummary = await this.readSummary(channel);
|
||||
const existingSummary = previousSummary?.summaryText || "";
|
||||
|
||||
// Ask LLM to summarize
|
||||
const prompt = existingSummary
|
||||
@@ -236,19 +244,11 @@ export class ContextStore {
|
||||
const summaryData: ContextSummary = {
|
||||
channel,
|
||||
summaryText: summaryText.slice(0, this.options.maxSummaryChars),
|
||||
entriesCompacted: entries.length + (existingSummary ? 1 : 0),
|
||||
entriesCompacted: (previousSummary?.entriesCompacted || 0) + entries.length,
|
||||
lastCompactedAt: new Date().toISOString(),
|
||||
totalCompactions: 1, // increment from existing
|
||||
totalCompactions: (previousSummary?.totalCompactions || 0) + 1,
|
||||
};
|
||||
|
||||
// Update total compactions
|
||||
try {
|
||||
const raw = await fs.readFile(this.summaryFile(channel), "utf-8");
|
||||
const prev: ContextSummary = JSON.parse(raw);
|
||||
summaryData.totalCompactions = (prev.totalCompactions || 0) + 1;
|
||||
summaryData.entriesCompacted = (prev.entriesCompacted || 0) + entries.length;
|
||||
} catch { /* first compaction */ }
|
||||
|
||||
await fs.writeFile(this.summaryFile(channel), JSON.stringify(summaryData, null, 2), "utf-8");
|
||||
|
||||
// Replace raw file with only recent entries
|
||||
|
||||
+31
-26
@@ -1,5 +1,6 @@
|
||||
import http from "node:http";
|
||||
import fs from "node:fs";
|
||||
import { promises as fsp } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
@@ -498,18 +499,20 @@ async function synthesizeTTS(
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (result.status === "completed" && fs.existsSync(outputPath)) {
|
||||
const audioBuffer = fs.readFileSync(outputPath);
|
||||
const base64 = audioBuffer.toString("base64");
|
||||
if (result.status === "completed") {
|
||||
try {
|
||||
const audioBuffer = await fsp.readFile(outputPath);
|
||||
const base64 = audioBuffer.toString("base64");
|
||||
|
||||
// Broadcast audio to channel
|
||||
broadcastFn(channel, { type: "audio", nick, data: base64, mimeType: "audio/wav" });
|
||||
// Broadcast audio to channel
|
||||
broadcastFn(channel, { type: "audio", nick, data: base64, mimeType: "audio/wav" });
|
||||
} catch { /* file missing — ignore */ }
|
||||
}
|
||||
} catch (err) {
|
||||
// TTS failure is non-critical, just log
|
||||
console.error(`[tts] Synthesis failed for ${nick}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(outputPath); } catch { /* ignore cleanup errors */ }
|
||||
try { await fsp.unlink(outputPath); } catch { /* ignore cleanup errors */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,8 +895,8 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (result.status === "completed" && fs.existsSync(outputPath)) {
|
||||
const audioBuffer = fs.readFileSync(outputPath);
|
||||
if (result.status === "completed") {
|
||||
const audioBuffer = await fsp.readFile(outputPath);
|
||||
const base64 = audioBuffer.toString("base64");
|
||||
|
||||
broadcast(info.channel, {
|
||||
@@ -911,7 +914,7 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
|
||||
type: "system",
|
||||
text: `[Musique generee: "${musicPrompt}"]`,
|
||||
});
|
||||
fs.unlinkSync(outputPath);
|
||||
fsp.unlink(outputPath).catch(() => {});
|
||||
} else {
|
||||
send(ws, { type: "system", text: `Composition echouee: ${result.error || "unknown"}` });
|
||||
}
|
||||
@@ -1237,7 +1240,7 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
|
||||
const tmpFile = path.join("/tmp", `kxkm-audio-${Date.now()}.${ext}`);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tmpFile, buffer);
|
||||
await fsp.writeFile(tmpFile, buffer);
|
||||
const scriptPath = path.resolve(
|
||||
process.env.SCRIPTS_DIR || path.join(process.cwd(), "scripts"),
|
||||
"transcribe_audio.py",
|
||||
@@ -1267,36 +1270,38 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
|
||||
} catch (err) {
|
||||
analysis = `[Audio: ${filename} — erreur: ${err instanceof Error ? err.message : String(err)}]`;
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpFile); } catch { /* ignore cleanup errors */ }
|
||||
try { await fsp.unlink(tmpFile); } catch { /* ignore cleanup errors */ }
|
||||
}
|
||||
} else if (mimeType === "application/pdf") {
|
||||
const tmpFile = path.join("/tmp", `kxkm-pdf-${Date.now()}.pdf`);
|
||||
try {
|
||||
fs.writeFileSync(tmpFile, buffer);
|
||||
await fsp.writeFile(tmpFile, buffer);
|
||||
await acquireFileProcessor();
|
||||
const pythonBin = process.env.PYTHON_BIN || "python3";
|
||||
const scriptPath = path.join(process.env.SCRIPTS_DIR || "scripts", "extract_pdf_docling.py");
|
||||
const { stdout, stderr } = await execFileAsync(pythonBin, [scriptPath, "--input", tmpFile], { timeout: 60_000 });
|
||||
releaseFileProcessor();
|
||||
if (stderr) console.log(`[upload] pdf: ${stderr.slice(-200)}`);
|
||||
const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
|
||||
if (result.text) {
|
||||
analysis = `[PDF: ${filename}, ${result.pages || "?"} page(s)]\n${result.text}`;
|
||||
} else {
|
||||
analysis = `[PDF: ${filename} — extraction échouée: ${result.error || "unknown"}]`;
|
||||
try {
|
||||
const pythonBin = process.env.PYTHON_BIN || "python3";
|
||||
const scriptPath = path.join(process.env.SCRIPTS_DIR || "scripts", "extract_pdf_docling.py");
|
||||
const { stdout, stderr } = await execFileAsync(pythonBin, [scriptPath, "--input", tmpFile], { timeout: 60_000 });
|
||||
if (stderr) console.log(`[upload] pdf: ${stderr.slice(-200)}`);
|
||||
const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
|
||||
if (result.text) {
|
||||
analysis = `[PDF: ${filename}, ${result.pages || "?"} page(s)]\n${result.text}`;
|
||||
} else {
|
||||
analysis = `[PDF: ${filename} — extraction échouée: ${result.error || "unknown"}]`;
|
||||
}
|
||||
} finally {
|
||||
releaseFileProcessor();
|
||||
}
|
||||
} catch (err) {
|
||||
releaseFileProcessor();
|
||||
analysis = `[PDF: ${filename} — erreur: ${err instanceof Error ? err.message : String(err)}]`;
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpFile); } catch {}
|
||||
try { await fsp.unlink(tmpFile); } catch {}
|
||||
}
|
||||
} else if (isOfficeDocument(filename, mimeType)) {
|
||||
// Word, Excel, PowerPoint, LibreOffice, RTF, EPUB
|
||||
const ext = filename.split(".").pop() || "";
|
||||
const tmpFile = path.join("/tmp", `kxkm-doc-${Date.now()}.${ext}`);
|
||||
try {
|
||||
fs.writeFileSync(tmpFile, buffer);
|
||||
await fsp.writeFile(tmpFile, buffer);
|
||||
const pythonBin = process.env.PYTHON_BIN || "python3";
|
||||
const scriptPath = path.join(process.env.SCRIPTS_DIR || "scripts", "extract_document.py");
|
||||
await acquireFileProcessor();
|
||||
@@ -1318,7 +1323,7 @@ export function attachWebSocketChat(server: http.Server, options: ChatOptions):
|
||||
} catch (err) {
|
||||
analysis = `[Document: ${filename} — erreur: ${err instanceof Error ? err.message : String(err)}]`;
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
||||
try { await fsp.unlink(tmpFile); } catch { /* ignore */ }
|
||||
}
|
||||
} else {
|
||||
analysis = `[Fichier: ${filename}, type: ${mimeType}, ${(size / 1024).toFixed(0)} KB]`;
|
||||
|
||||
@@ -16,6 +16,7 @@ import NodeEditor from "./components/NodeEditor";
|
||||
import TrainingDashboard from "./components/TrainingDashboard";
|
||||
import Analytics from "./components/Analytics";
|
||||
import Collectif from "./components/Collectif";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
|
||||
function parseHash(): { page: string; id: string } {
|
||||
const hash = window.location.hash.replace(/^#\/?/, "");
|
||||
@@ -188,7 +189,9 @@ export default function App() {
|
||||
<Nav currentPage={route.page} session={session} onNavigate={navigate} />
|
||||
<main className="app-main">
|
||||
{error && <div className="banner">{error}</div>}
|
||||
{renderPage()}
|
||||
<ErrorBoundary>
|
||||
{renderPage()}
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -495,7 +495,7 @@ export default function Chat() {
|
||||
</div>
|
||||
|
||||
<div className="chat-body">
|
||||
<div className="chat-messages" ref={messagesContainerRef}>
|
||||
<div className="chat-messages" ref={messagesContainerRef} role="log" aria-live="polite">
|
||||
{messages.map((msg) => (
|
||||
<ChatMessage key={msg.id} msg={msg} getNickColor={getNickColor} channel={channel} />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component, type ReactNode, type ErrorInfo } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false, error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error("[ErrorBoundary]", error, info.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) return this.props.fallback;
|
||||
return (
|
||||
<div style={{
|
||||
padding: "2rem",
|
||||
background: "#1a0a0a",
|
||||
color: "#ff6b6b",
|
||||
border: "1px solid #ff6b6b",
|
||||
fontFamily: "monospace",
|
||||
margin: "1rem",
|
||||
}}>
|
||||
<h2 style={{ margin: "0 0 1rem" }}>{">>> ERREUR SYSTEME <<<"}</h2>
|
||||
<pre style={{ whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "#ccc" }}>
|
||||
{this.state.error?.message || "Erreur inconnue"}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null })}
|
||||
style={{
|
||||
marginTop: "1rem",
|
||||
padding: "0.5rem 1rem",
|
||||
background: "#333",
|
||||
color: "#0f0",
|
||||
border: "1px solid #0f0",
|
||||
cursor: "pointer",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
F3=Retour
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
+126
-28
@@ -1,40 +1,138 @@
|
||||
# Agents, Sous-agents, Competences
|
||||
|
||||
> "L'infrastructure est une decision politique deployee." -- electron rare
|
||||
|
||||
## Orchestration
|
||||
|
||||
- Agent racine: Coordinateur
|
||||
- Sous-agent analyse code: Explore (thorough)
|
||||
- Sous-agent veille OSS: Explore + fetch web
|
||||
- Cadence: synchroniser PLAN.md + TODO.md + docs/PROJECT_MEMORY.md apres chaque lot
|
||||
- Agent racine: **Coordinateur** — planifie, arbitre, synchronise PLAN/TODO/docs
|
||||
- Sous-agents specialises: analyse code, veille OSS, audit securite, optimisation
|
||||
- Cadence: synchroniser PLAN.md + TODO.md + docs apres chaque lot
|
||||
|
||||
## Matrice des agents
|
||||
## Matrice des agents (lot 17+)
|
||||
|
||||
| Agent | Competences | Taches actives | Etat |
|
||||
| Agent | Competences | Perimetre | Etat |
|
||||
|---|---|---|---|
|
||||
| Coordinateur | planification, arbitrage, docs de pilotage | aligner plan/todo/docs avec etat reel | en cours |
|
||||
| Securite | validation input, hardening runtime, limites | verifier invariants runtime + rate-limit scope | en cours |
|
||||
| Backend V1/V2 | express/ws/api contracts, ollama integration | contrat storage API/worker, health contract | en cours |
|
||||
| Node Engine | DAG, queue, runs, sandbox | robustesse pipeline + test parite schema | a enchainer |
|
||||
| Personas | source/feedback/proposals/pharmacius | clarifier trace editoriale et exports training | a enchainer |
|
||||
| Frontend | React/Vite, UX IRC, React Flow | consolidations surfaces chat/admin/engine | a enchainer |
|
||||
| Ops/TUI | scripts, logs, rotate/purge, observabilite | run lot suivant + hygiene logs | en cours |
|
||||
| Migration | parity, migrate, rollback | rehearsal migration V1->V2 sur Postgres local | a enchainer |
|
||||
| Coordinateur | planification, arbitrage, docs de pilotage | PLAN.md, TODO.md, AGENTS.md, README.md | actif |
|
||||
| Securite | validation input, hardening, rate-limit, RBAC | apps/api, ws-chat, packages/auth | veille |
|
||||
| Backend API | Express, WS, Ollama, RAG, multimodal pipeline | apps/api/src/ | actif |
|
||||
| Node Engine | DAG, queue, runs, sandbox, training adapters | packages/node-engine, apps/worker | actif |
|
||||
| Personas | source/feedback/proposals/pharmacius, memoire | packages/persona-domain, ws-chat | actif |
|
||||
| Frontend | React/Vite, UX Minitel, React Flow, chat, voice | apps/web/src/ | actif |
|
||||
| Ops/TUI | scripts, logs, rotate/purge, health, audit | ops/v2/, scripts/ | actif |
|
||||
| Training | DPO, SFT, Unsloth, eval, autoresearch, Ollama import | scripts/, packages/node-engine | actif |
|
||||
| Multimodal | STT, TTS, vision, PDF, RAG, recherche web | apps/api/src/ws-chat.ts | actif |
|
||||
| Veille OSS | recherche projets, libs, modeles, benchmarks | docs/OSS_WATCH, docs/HF_MODEL_RESEARCH | periodique |
|
||||
|
||||
## Sous-agents et skill routing
|
||||
|
||||
- Explore (audit): differences docs vs code, quick wins, risques
|
||||
- Explore (veille): OSS comparables (ollama-js, bullmq, rete, flowise, node-red, localai)
|
||||
- Skill resume issue/pr: a utiliser pour synthese PR/issue
|
||||
- Skill suggest-fix-issue: a utiliser pour proposition de correction ciblee
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Coord[Coordinateur]
|
||||
|
||||
## Todo agents immediats
|
||||
Coord --> SecAgent[Securite]
|
||||
Coord --> BackAgent[Backend API]
|
||||
Coord --> EngAgent[Node Engine]
|
||||
Coord --> PersAgent[Personas]
|
||||
Coord --> FrontAgent[Frontend]
|
||||
Coord --> OpsAgent[Ops/TUI]
|
||||
Coord --> TrainAgent[Training]
|
||||
Coord --> MultiAgent[Multimodal]
|
||||
Coord --> OSSAgent[Veille OSS]
|
||||
|
||||
- Coordinateur:
|
||||
- fermer incoherences TODO/PLAN/README
|
||||
- maintenir priorites lot-3 et lot-4
|
||||
- Backend:
|
||||
- maintenir garde-fou DB en prod
|
||||
- harmoniser communication contrat storage
|
||||
- Ops/TUI:
|
||||
- executer lot-3
|
||||
- analyser logs puis purger logs temporaires
|
||||
SecAgent --> |audit| SecScan[Pattern scan P0/P1/P2]
|
||||
SecAgent --> |fix| SecFix[Correctifs chirurgicaux]
|
||||
|
||||
BackAgent --> |analyse| APIAudit[Deep analyse app.ts, ws-chat.ts]
|
||||
BackAgent --> |refactor| APISplit[Extraction modules]
|
||||
|
||||
EngAgent --> |test| EngTest[Tests unitaires node-engine]
|
||||
EngAgent --> |extend| EngNew[Nouveaux node types]
|
||||
|
||||
PersAgent --> |pipeline| PersPipe[Editorial pipeline]
|
||||
PersAgent --> |finetune| PersDPO[DPO + PCL methodology]
|
||||
|
||||
FrontAgent --> |ui| FrontUI[Minitel theme CSS]
|
||||
FrontAgent --> |perf| FrontPerf[Memoization, lazy load]
|
||||
|
||||
OpsAgent --> |monitor| OpsHealth[health-check, deep-audit]
|
||||
OpsAgent --> |deploy| OpsDeploy[Docker, kxkm-ai]
|
||||
|
||||
TrainAgent --> |train| TrainRun[Unsloth/TRL runs]
|
||||
TrainAgent --> |eval| TrainEval[Scoring, registry]
|
||||
|
||||
MultiAgent --> |voice| MultiVoice[XTTS-v2, WebRTC]
|
||||
MultiAgent --> |search| MultiSearch[SearXNG]
|
||||
|
||||
OSSAgent --> |web| OSSWeb[Recherche web]
|
||||
OSSAgent --> |hf| OSSHF[HuggingFace models]
|
||||
```
|
||||
|
||||
## Todo agents (lot 17)
|
||||
|
||||
### Coordinateur
|
||||
|
||||
- [ ] Consolider PLAN.md avec etat reel (lots 14-16 complets)
|
||||
- [ ] Mettre a jour TODO.md avec backlog Phase 6+
|
||||
- [ ] Synchroniser FEATURE_MAP.md matrice
|
||||
- [ ] Documenter actions dans ops/v2/logs/
|
||||
|
||||
### Backend API
|
||||
|
||||
- [ ] Refactorer ws-chat.ts (1449 → <400 LOC par module)
|
||||
- [ ] Refactorer app.ts (1292 → routes + middleware + handlers)
|
||||
- [ ] Remplacer writeFileSync par async dans ws-chat.ts
|
||||
- [ ] Ajouter error boundaries sur WebSocket handlers
|
||||
|
||||
### Node Engine
|
||||
|
||||
- [ ] Ajouter node type `music_generation` (ACE-Step 1.5)
|
||||
- [ ] Ajouter node type `voice_clone` (XTTS-v2)
|
||||
- [ ] Tester pipeline DPO end-to-end sur kxkm-ai
|
||||
|
||||
### Personas
|
||||
|
||||
- [ ] Evaluer PCL (Persona-Aware Contrastive Learning) pour coherence
|
||||
- [ ] Evaluer OpenCharacter pour generation profils synthetiques
|
||||
- [ ] Ajouter `/compose` command (generation musicale)
|
||||
|
||||
### Frontend
|
||||
|
||||
- [ ] Implementer lot 16 UI Minitel rose (phosphore, VIDEOTEX)
|
||||
- [ ] Ajouter memoization (React.memo) sur composants lourds
|
||||
- [ ] Lazy-load ChatHistory, VoiceChat, NodeEditor
|
||||
|
||||
### Ops/TUI
|
||||
|
||||
- [ ] Deployer deep-audit.js sur kxkm-ai
|
||||
- [ ] Ajouter SearXNG au docker-compose
|
||||
- [ ] Ajouter MinerU/Docling au docker-compose
|
||||
|
||||
### Training
|
||||
|
||||
- [ ] Spike BGE-M3 embeddings (remplacer nomic-embed-text)
|
||||
- [ ] Tester ACE-Step 1.5 sur RTX 4090
|
||||
- [ ] Evaluer Chatterbox TTS vs Piper
|
||||
|
||||
### Veille OSS
|
||||
|
||||
- [ ] Suivre LLMRTC (WebRTC voice TypeScript)
|
||||
- [ ] Suivre A2A Protocol (interop agents)
|
||||
- [ ] Suivre MCP SDK updates
|
||||
|
||||
## Pipeline d'intervention
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Analyse: agent lance
|
||||
Analyse --> Findings: scan code/docs/web
|
||||
Findings --> Triage: P0/P1/P2 classification
|
||||
Triage --> Fix_P0: P0 critique
|
||||
Triage --> Plan_P1: P1 important
|
||||
Triage --> Backlog_P2: P2 mineur
|
||||
Fix_P0 --> Test: correction chirurgicale
|
||||
Plan_P1 --> Test: correction planifiee
|
||||
Test --> Deploy: tests OK
|
||||
Deploy --> Log: log + purge
|
||||
Log --> [*]: cycle termine
|
||||
Backlog_P2 --> [*]: ajoute au TODO
|
||||
```
|
||||
|
||||
@@ -546,3 +546,97 @@ flowchart TD
|
||||
| GET | `/api/v2/chat/history` | Liste fichiers de chat logs |
|
||||
| GET | `/api/v2/chat/history/:date` | Messages d'un jour (pagine) |
|
||||
| POST | `/api/v2/admin/retention-sweep` | Nettoyage runs anciens |
|
||||
|
||||
---
|
||||
|
||||
## 13. Context Store & Memoire persistante
|
||||
|
||||
Flux de compaction automatique du contexte conversationnel par canal.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Msg["Message entrant (append)"]
|
||||
Msg --> JSONL["channel.jsonl (append line)"]
|
||||
JSONL --> SizeCheck{"Taille > 1 MB\net > 500 entries?"}
|
||||
SizeCheck -- non --> Done["Fin"]
|
||||
SizeCheck -- oui --> Split["Split 80/20"]
|
||||
|
||||
Split --> Old["80% anciennes entries"]
|
||||
Split --> Recent["20% recentes entries"]
|
||||
|
||||
Old --> Summarize["LLM summarization\n(qwen3.5:9b via Ollama)"]
|
||||
|
||||
subgraph Summary["Compaction"]
|
||||
Summarize --> Merge["Fusionner avec\nsummary existant"]
|
||||
Merge --> Save["channel.summary.json"]
|
||||
end
|
||||
|
||||
Recent --> Rewrite["Reecrire channel.jsonl\n(entries recentes seules)"]
|
||||
|
||||
Save --> Done2["Compaction terminee"]
|
||||
Rewrite --> Done2
|
||||
```
|
||||
|
||||
### Injection contexte dans le prompt
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Load["getContext(channel)"]
|
||||
Load --> ReadSummary["Lire channel.summary.json"]
|
||||
Load --> ReadRecent["Lire channel.jsonl (tail)"]
|
||||
|
||||
ReadSummary --> Combine["Combiner:\nResume + Echanges recents"]
|
||||
ReadRecent --> Combine
|
||||
|
||||
Combine --> Cap["Tronquer a maxContextChars\n(default 16K)"]
|
||||
Cap --> Inject["Injecter dans systemPrompt"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Deploiement Docker
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Docker["Docker Compose"]
|
||||
API["api:4180\n(Express + WS + RAG)"]
|
||||
Worker["worker\n(poll Postgres)"]
|
||||
Postgres["postgres:5432\n(persistence)"]
|
||||
V1["v1:3333\n(reference, optionnel)"]
|
||||
end
|
||||
|
||||
subgraph Host["Host (kxkm-ai)"]
|
||||
Ollama["Ollama:11434\n(natif, RTX 4090)"]
|
||||
Venv["Python venv\n(PyTorch, faster-whisper,\npiper-tts, Unsloth)"]
|
||||
end
|
||||
|
||||
API --> Postgres
|
||||
Worker --> Postgres
|
||||
API --> Ollama
|
||||
Worker --> Ollama
|
||||
API --> Venv
|
||||
|
||||
Browser["Client Browser"] --> API
|
||||
Browser --> V1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Inter-persona dialogue
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Utilisateur
|
||||
participant WS as ws-chat
|
||||
participant P1 as Persona A
|
||||
participant P2 as Persona B (@mention)
|
||||
|
||||
U->>WS: "Hey @Schaeffer, que penses-tu?"
|
||||
WS->>P1: route vers Schaeffer
|
||||
P1-->>WS: reponse (mentionne @Radigue)
|
||||
WS->>WS: detecter @mention dans reponse
|
||||
WS->>P2: route vers Radigue (depth 2)
|
||||
P2-->>WS: reponse (depth 2)
|
||||
WS->>WS: depth >= 3? stop
|
||||
WS-->>U: broadcast toutes les reponses
|
||||
```
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# Deep Audit Report — 2026-03-17
|
||||
|
||||
> "Le reel n'est jamais propre, mais il est toujours testable." -- electron rare
|
||||
|
||||
## Scope
|
||||
|
||||
Audit complet du monorepo kxkm_clown + repo mascarade/core.
|
||||
5 agents paralleles (api, web, packages, mascarade, v1+worker) + TUI deep-audit.js.
|
||||
|
||||
## Metriques globales
|
||||
|
||||
| Metrique | Valeur |
|
||||
| --- | --- |
|
||||
| Erreurs TypeScript | **0** |
|
||||
| Tests | **119 pass, 0 fail** |
|
||||
| Security findings P0 | **0** (apres corrections) |
|
||||
| Fichiers >500 LOC | **8** |
|
||||
| Packages outdated | **2** |
|
||||
|
||||
## Corrections appliquees (chirurgicales)
|
||||
|
||||
### apps/api (P0)
|
||||
|
||||
| Fix | Sev | Detail |
|
||||
| --- | --- | --- |
|
||||
| Semaphore double-release PDF | P0 | try/finally restructure, un seul releaseFileProcessor() |
|
||||
| Path traversal voice-sample | P0 | path.basename() + boundary check 3 endpoints |
|
||||
| Analytics sans auth | P1 | requirePermission("ops:read") |
|
||||
| 5x sync I/O handlers async | P2 | fsp.writeFile/readFile/unlink |
|
||||
|
||||
### apps/web (P0)
|
||||
|
||||
| Fix | Sev | Detail |
|
||||
| --- | --- | --- |
|
||||
| Pas d'Error Boundary | P0 | ErrorBoundary.tsx cree + integre dans App.tsx |
|
||||
| Chat non accessible | P1 | role="log" aria-live="polite" |
|
||||
|
||||
### packages (P0)
|
||||
|
||||
| Fix | Sev | Detail |
|
||||
| --- | --- | --- |
|
||||
| core createId() non-crypto | P0 | Math.random → crypto.randomBytes(8) hex |
|
||||
| auth timing leak verifyPassword | P0 | Buffer padding avant timingSafeEqual |
|
||||
| persona-domain applyPatches unsafe | P0 | Allowlist fields + typeof type guard |
|
||||
|
||||
## Findings non-corriges (backlog)
|
||||
|
||||
### apps/api — P1
|
||||
|
||||
- ws-chat.ts: 1449 LOC (refactoring planifie → 4 modules)
|
||||
- app.ts: 1292 LOC (extraction routes planifiee)
|
||||
- Persona memory race condition (concurrent updatePersonaMemory)
|
||||
- Session expiration not enforced (no periodic cleanup)
|
||||
- RAG O(n) linear search (candidat HNSW/Annoy)
|
||||
- Context store unlimited concurrent compactions
|
||||
|
||||
### apps/web — P1
|
||||
|
||||
- No React.memo on large components (Chat, ChatHistory, VoiceChat)
|
||||
- No virtualization for message list (max 500 DOM nodes)
|
||||
- Hash routing fragile (no route guards, no 404)
|
||||
- Tab completion O(n) filtering on each keystroke
|
||||
- WebSocket send silently fails if disconnected
|
||||
- File upload no size validation before base64 encoding
|
||||
|
||||
### packages — P1
|
||||
|
||||
- **storage: 0 tests** (P0 — couverture critique manquante)
|
||||
- node-engine sandbox.ts: 0 tests
|
||||
- node-engine training.ts: 0 tests
|
||||
- chat-domain channel normalization returns empty string
|
||||
- persona-domain extractJsonBlock greedy regex
|
||||
|
||||
### mascarade (repo companion)
|
||||
|
||||
- server.py (52 KB), cluster.py (41 KB): fichiers volumineux
|
||||
- MCP client FreeCAD sandboxing fragile (blocklist vs AST)
|
||||
- P2P: deux implementations (asyncio vs libp2p) — maintenance burden
|
||||
- 190+ settings dans config.py sans validation conflits
|
||||
|
||||
## Fichiers complexite elevee
|
||||
|
||||
| Fichier | LOC | Status |
|
||||
| --- | --- | --- |
|
||||
| apps/api/src/ws-chat.ts | 1449 | refactoring planifie |
|
||||
| apps/api/src/app.ts | 1292 | refactoring planifie |
|
||||
| packages/persona-domain/src/index.ts | 782 | acceptable |
|
||||
| packages/node-engine/src/index.ts | 717 | acceptable |
|
||||
| apps/web/src/components/ChatHistory.tsx | 602 | a surveiller |
|
||||
| apps/web/src/components/Chat.tsx | 567 | a surveiller |
|
||||
| packages/storage/src/index.ts | 551 | acceptable |
|
||||
| apps/worker/src/index.ts | 548 | acceptable |
|
||||
|
||||
## Findings V1 + Worker (agent 5/5)
|
||||
|
||||
### V1 Code Quality: GOOD
|
||||
|
||||
- 27 modules V1 bien separes (chat-routing, commands, http-api, websocket, storage, etc.)
|
||||
- smoke.js: 1199 lignes de tests d'integration comprehensive
|
||||
- Ops TUI (health-check, queue-viewer, persona-manager, log-rotate): EXCELLENT quality
|
||||
|
||||
### Issues V1
|
||||
|
||||
| Issue | Sev | Detail |
|
||||
| --- | --- | --- |
|
||||
| Memory leak channelHistory | P1 | Map grows unbounded, no LRU eviction |
|
||||
| Unhandled Promise in websocket.js | P1 | Async handlers not awaited in some paths |
|
||||
| Docker runs as root | P1 | Missing USER node in Dockerfile |
|
||||
| safeCompare() duplique | P2 | commands.js + http-api.js (extract to util) |
|
||||
| Dead code workspace-package.js | P2 | **Supprime** (9 lignes, exports nothing) |
|
||||
| ANSI helpers dupliques | P2 | 4+ scripts ops (consolider dans @kxkm/tui) |
|
||||
| No graceful shutdown server.js | P2 | Missing SIGTERM/SIGINT handlers |
|
||||
|
||||
### Mascarade (repo companion)
|
||||
|
||||
- Orchestrateur LLM sophistique: 15 providers, P2P, MCP, fine-tuning
|
||||
- server.py (52 KB), cluster.py (41 KB): fichiers volumineux
|
||||
- Integration KXKM: coexistence Docker, partage Ollama, MCP potentiel
|
||||
|
||||
## Recommandations prioritaires
|
||||
|
||||
### Immediat (lot 17 suite)
|
||||
|
||||
1. **Refactoring ws-chat.ts** → ws-multimodal.ts, ws-persona-router.ts, ws-commands.ts
|
||||
2. **Refactoring app.ts** → routes/personas.ts, routes/node-engine.ts, routes/chat.ts
|
||||
3. **Tests storage** — couverture minimale repos Postgres
|
||||
4. **Tests sandbox.ts + training.ts** — couverture node-engine
|
||||
|
||||
### Moyen terme (lot 18)
|
||||
|
||||
5. SearXNG dans docker-compose
|
||||
6. Discord Pharmacius bot
|
||||
7. React.memo + lazy load composants lourds
|
||||
8. RAG: evaluer BGE-M3 embeddings
|
||||
|
||||
### Long terme (lot 19+)
|
||||
|
||||
9. Virtualization messages chat (react-window)
|
||||
10. HNSW index pour RAG search
|
||||
11. MCP integration personas ↔ outils
|
||||
12. XTTS-v2 voice cloning
|
||||
@@ -133,3 +133,126 @@ Sources : [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescrip
|
||||
- **Pour KXKM** : concept seduisant — chaque persona KXKM pourrait publier un Agent Card et communiquer via A2A. Mais le protocole est encore immature (RC), et le overhead HTTPS/JSON-RPC est lourd pour des personas locales qui partagent deja un bus WebSocket. A surveiller pour interop avec des agents externes, pas pour la communication interne.
|
||||
|
||||
Sources : [A2A Specification](https://a2a-protocol.org/latest/specification/) | [Google A2A Blog](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/) | [A2A GitHub](https://github.com/a2aproject/A2A)
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-03-17 (deep audit session)
|
||||
|
||||
### 4. Voice Cloning & TTS — Alternatives a Piper
|
||||
|
||||
| Projet | Signal observe | Usage potentiel |
|
||||
| --- | --- | --- |
|
||||
| Coqui XTTS-v2 | Zero-shot voice cloning, 16 langues, reference quality | voix unique par persona via 6s de reference audio |
|
||||
| OpenVoice v2 | Leger, zero-shot, controle emotion/accent/rythme | alternative rapide pour prototypage voix |
|
||||
| Chatterbox | #1 trending HuggingFace TTS, bon ratio qualite/vitesse | candidat remplacement Piper |
|
||||
| StyleTTS2 | Studio-quality narration, diffusion-based | personas "premium" narration longue |
|
||||
| Bark | Transformer generatif, expressif, non-speech sounds | personas experimentales (rire, chant, bruits) |
|
||||
|
||||
**Recommandation** : deployer XTTS-v2 pour le clonage vocal personas (lot Phase 6). Garder Piper pour le fallback leger temps reel.
|
||||
|
||||
Sources : [Coqui XTTS-v2](https://huggingface.co/coqui/XTTS-v2) | [OpenVoice](https://github.com/myshell-ai/OpenVoice) | [Chatterbox](https://huggingface.co/resemble-ai/chatterbox) | [Resemble AI Guide](https://www.resemble.ai/best-open-source-ai-voice-cloning-tools/)
|
||||
|
||||
### 5. Music Generation — ACE-Step 1.5
|
||||
|
||||
| Projet | Signal observe | Usage potentiel |
|
||||
| --- | --- | --- |
|
||||
| ACE-Step 1.5 | Commercial-grade, <4GB VRAM, <10s RTX 3090, outperforms Suno v5 | generation musicale Node Engine |
|
||||
| MusicGen (Meta) | Text/melody → musique, models 1.5B/3.3B | alternative stable si ACE-Step trop lourd |
|
||||
|
||||
**ACE-Step 1.5** (janvier 2026) : modele LM 0.6B-4B, Chain-of-Thought song blueprint, lyrics-conditioned. Ideal pour `/compose` command — generer de la musique concrete via prompts textuels.
|
||||
|
||||
Sources : [ACE-Step 1.5 GitHub](https://github.com/ace-step/ACE-Step-1.5) | [ACE-Step Paper](https://arxiv.org/abs/2602.00744) | [MusicGen](https://huggingface.co/facebook/musicgen-large)
|
||||
|
||||
### 6. PDF Extraction — Alternatives a pdf-parse
|
||||
|
||||
| Projet | Signal observe | Usage potentiel |
|
||||
| --- | --- | --- |
|
||||
| Docling (IBM) | Layout analysis + TableFormer, open source | remplacer pdf-parse pour tables/layout |
|
||||
| MinerU | 84 langues OCR, heading detection, tables HTML, formulas | meilleur sur docs complexes |
|
||||
| Dolphin (ByteDance) | Vision Transformer OCR, layout preservation | alternative pour documents scannes |
|
||||
| OpenDataLoader | #1 benchmark overall (0.90), bounding boxes | meilleur precision globale |
|
||||
|
||||
**Recommandation** : MinerU comme premier choix (meilleur sur tables complexes + OCR 84 langues). Docling en fallback. OpenDataLoader a surveiller.
|
||||
|
||||
Sources : [Docling GitHub](https://github.com/docling-project/docling) | [MinerU](https://github.com/opendatalab/PDF-Extract-Kit) | [Benchmark](https://jimmysong.io/blog/pdf-to-markdown-open-source-deep-dive/)
|
||||
|
||||
### 7. SearXNG — Moteur de recherche self-hosted
|
||||
|
||||
- **Docker** : `searxng/searxng` image officielle, port 8080, ~300MB, tourne sur Raspberry Pi 4
|
||||
- **Config** : `settings.yml` pour activer/desactiver les engines, format JSON API
|
||||
- **Integration KXKM** : remplacer DuckDuckGo Lite scraping par `http://searxng:8080/search?q=X&format=json`
|
||||
- **Docker Compose** : ajouter service `searxng` avec volume config persistant
|
||||
|
||||
Sources : [SearXNG Docker](https://github.com/searxng/searxng-docker) | [SearXNG Docs](https://docs.searxng.org/admin/installation-docker.html)
|
||||
|
||||
### 8. RAG Local — Libs TypeScript
|
||||
|
||||
| Projet | Signal observe | Usage potentiel |
|
||||
| --- | --- | --- |
|
||||
| embedJs | Node.js RAG framework, LLM-agnostic, embeddings | remplacement RAG custom si trop complexe |
|
||||
| LangChain.js | Vector stores, document loaders, splitters, TypeScript | framework RAG complet, overhead |
|
||||
|
||||
**Embedding models recommandes** :
|
||||
|
||||
- `nomic-embed-text` (actuel) — bon rapport qualite/taille
|
||||
- `BGE-M3` — meilleur score MTEB (63.0), MIT license, production-ready
|
||||
- `all-minilm:l6-v2` — ultra-leger pour prototypage
|
||||
|
||||
**Recommandation** : garder le RAG custom actuel (simple, pas d'overhead framework). Evaluer BGE-M3 comme upgrade du modele d'embedding.
|
||||
|
||||
### 9. WebRTC Voice — Streaming temps reel
|
||||
|
||||
| Projet | Signal observe | Usage potentiel |
|
||||
| --- | --- | --- |
|
||||
| LLMRTC | TypeScript SDK, WebRTC, provider-agnostic, open source | SDK ideal pour WebRTC voice KXKM |
|
||||
| FastRTC (HuggingFace) | Python, real-time audio/video, VAD, Gradio | backend Python pour STT/TTS streaming |
|
||||
| Pipecat | Orchestrator frames, local LLM + STT + TTS, WebRTC | architecture reference voice pipeline |
|
||||
|
||||
**LLMRTC** est le meilleur candidat : TypeScript natif, WebRTC low-latency, VAD, barge-in, sentence-boundary TTS. Integration directe dans le WebSocket existant.
|
||||
|
||||
Sources : [LLMRTC](https://www.llmrtc.org/) | [FastRTC](https://huggingface.co/blog/fastrtc) | [Pipecat Voice Agent](https://webrtc.ventures/2026/02/building-an-open-source-voice-ai-agent-that-avoids-vendor-lock-in/)
|
||||
|
||||
### 10. Persona Fine-Tuning — Methodologies 2026
|
||||
|
||||
| Methode | Source | Application |
|
||||
| --- | --- | --- |
|
||||
| OpenCharacter | arXiv 2501.15427, large-scale synthetic personas | generation de profils persona synthetiques |
|
||||
| PCL (Persona-Aware Contrastive Learning) | arXiv 2503.17662, annotation-free | ameliorer la coherence de role sans annotations |
|
||||
| PIKA | arXiv 2510.06670, expert-level synthetic datasets | datasets post-training alignment |
|
||||
| Constitutional AI for personas | arXiv 2511.01689, introspective data pipeline | shaping persona via constitution, pas prompt |
|
||||
|
||||
**Recommandation** : combiner OpenCharacter (generation profils) + PCL (consistency DPO) pour le pipeline fine-tune personas KXKM. Le DPO actuel est deja aligne — ajouter une passe PCL contrastive pour renforcer la coherence de role.
|
||||
|
||||
Sources : [OpenCharacter](https://arxiv.org/html/2501.15427v1) | [PCL](https://arxiv.org/html/2503.17662v1) | [PIKA](https://arxiv.org/html/2510.06670v1)
|
||||
|
||||
### 11. IRC Aesthetic Chat — Projets proches
|
||||
|
||||
| Projet | Signal observe | Usage potentiel |
|
||||
| --- | --- | --- |
|
||||
| Ollamarama-IRC | AI chatbot IRC, Ollama, multi-personalities | reference IRC + multi-persona local |
|
||||
| Soulshack | IRC bot, multi-provider, MCP tools, shell access | reference IRC + tool-calling |
|
||||
|
||||
Sources : [Ollamarama-IRC](https://github.com/h1ddenpr0cess20/ollamarama-irc) | [Soulshack](https://github.com/pkdindustries/soulshack)
|
||||
|
||||
---
|
||||
|
||||
## Recommandations consolidees 2026-03-17
|
||||
|
||||
### Actions immediates (lot courant)
|
||||
|
||||
1. **SearXNG** : ajouter au docker-compose, remplacer DuckDuckGo scraping
|
||||
2. **MinerU/Docling** : remplacer pdf-parse pour extraction PDF avancee
|
||||
3. **BGE-M3** : tester comme upgrade embedding model RAG
|
||||
|
||||
### Moyen terme (lots 17-18)
|
||||
|
||||
1. **XTTS-v2** : deployer voice cloning pour personas
|
||||
2. **LLMRTC** : spike WebRTC voice streaming
|
||||
3. **ACE-Step 1.5** : integrer `/compose` dans Node Engine
|
||||
4. **MCP SDK** : personas comme MCP servers (tool-calling)
|
||||
|
||||
### Long terme (recherche)
|
||||
|
||||
1. **PCL + OpenCharacter** : pipeline fine-tune persona avance
|
||||
2. **Chatterbox** : evaluer comme remplacement Piper (meilleur qualite)
|
||||
3. **A2A Protocol** : interop agents externes quand le protocole est stable
|
||||
|
||||
+65
-2
@@ -1,23 +1,86 @@
|
||||
# PLAN (kxkm-clown-v2)
|
||||
|
||||
Updated: 2026-03-16T06:48:45Z
|
||||
Updated: 2026-03-17T08:45:00Z
|
||||
|
||||
## lot-0-cadrage [done]
|
||||
|
||||
- Description: Docs, architecture, feature map, agents, invariants, orchestration
|
||||
- Depends on: none
|
||||
|
||||
## lot-1-socle [done]
|
||||
|
||||
- Description: Workspace V2, packages, scripts TUI, verification
|
||||
- Depends on: lot-0-cadrage
|
||||
|
||||
## lot-2-domaines [done]
|
||||
|
||||
- Description: Auth, chat, storage, personas, node engine
|
||||
- Depends on: lot-1-socle
|
||||
|
||||
## lot-3-surfaces [done]
|
||||
|
||||
- Description: Shell React/Vite, admin, chat, node engine, ops
|
||||
- Depends on: lot-2-domaines
|
||||
|
||||
## lot-4-bascule [done]
|
||||
- Description: Migration, parité, rollback, bascule
|
||||
|
||||
- Description: Migration, parite, rollback, bascule
|
||||
- Depends on: lot-3-surfaces
|
||||
|
||||
## lot-5-production [done]
|
||||
|
||||
- Description: Training adapters, sandboxing, tests, turborepo, CI/CD, GitHub
|
||||
- Depends on: lot-4-bascule
|
||||
|
||||
## lot-6-consolidation [done]
|
||||
|
||||
- Description: Deep analyse, correctifs securite, feature parity V2, deploy
|
||||
- Depends on: lot-5-production
|
||||
|
||||
## lot-7-training [done]
|
||||
|
||||
- Description: PyTorch, Unsloth, TRL, DPO pipeline, autoresearch, Ollama import
|
||||
- Depends on: lot-6-consolidation
|
||||
|
||||
## lot-8-multimodal [done]
|
||||
|
||||
- Description: RAG, STT, TTS, vision, PDF, recherche web, memoire persona
|
||||
- Depends on: lot-7-training
|
||||
|
||||
## lot-9-chat-avance [done]
|
||||
|
||||
- Description: Chat vocal, inter-persona, multi-channel, analytics, 26 personas
|
||||
- Depends on: lot-8-multimodal
|
||||
|
||||
## lot-10-generation [done]
|
||||
|
||||
- Description: ComfyUI, Sherlock, Picasso, diversification modeles, contexte 750MB
|
||||
- Depends on: lot-9-chat-avance
|
||||
|
||||
## lot-11-mcp-personas [done]
|
||||
|
||||
- Description: MCP tool-calling personas, pipeline fine-tune
|
||||
- Depends on: lot-10-generation
|
||||
|
||||
## lot-12-deep-audit [in-progress]
|
||||
|
||||
- Description: Deep audit code, refactoring, veille OSS, diagrammes, infrastructure
|
||||
- Depends on: lot-11-mcp-personas
|
||||
- Deliverables:
|
||||
- ops/v2/deep-audit.js (TUI security/perf/complexity)
|
||||
- docs/OSS_WATCH enrichi (voice, music, PDF, WebRTC, MCP, persona fine-tune)
|
||||
- docs/ARCHITECTURE.md (3 Mermaid ajoutes)
|
||||
- docs/AGENTS.md (matrice 10 agents, pipeline intervention)
|
||||
- PLAN.md + TODO.md consolides
|
||||
- Refactoring ws-chat.ts + app.ts
|
||||
- SearXNG + MinerU/Docling docker
|
||||
|
||||
## lot-13-voice-mcp [planned]
|
||||
|
||||
- Description: XTTS-v2 voice cloning, LLMRTC WebRTC, MCP SDK, Discord Pharmacius
|
||||
- Depends on: lot-12-deep-audit
|
||||
|
||||
## lot-14-music-creative [planned]
|
||||
|
||||
- Description: ACE-Step 1.5, /compose, Flux 2, A2A Protocol
|
||||
- Depends on: lot-13-voice-mcp
|
||||
|
||||
+102
-16
@@ -1,28 +1,114 @@
|
||||
# TODO (kxkm-clown-v2)
|
||||
|
||||
Updated: 2026-03-16T06:48:45Z
|
||||
Updated: 2026-03-17T08:45:00Z
|
||||
|
||||
## lot-0-cadrage
|
||||
- [x] architecture (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-0-cadrage/architecture.csv
|
||||
- [x] docs (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-0-cadrage/docs.csv
|
||||
- [x] agents (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-0-cadrage/agents.csv
|
||||
|
||||
- [x] architecture (done)
|
||||
- [x] docs (done)
|
||||
- [x] agents (done)
|
||||
|
||||
## lot-1-socle
|
||||
- [x] workspace (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-1-socle/workspace.csv
|
||||
- [x] verification (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-1-socle/verification.csv
|
||||
- [x] ops-tui (done) | out: /home/kxkm/KXKM_Clown/ops/v2/outputs/lot-1-socle/ops-tui.csv
|
||||
|
||||
- [x] workspace (done)
|
||||
- [x] verification (done)
|
||||
- [x] ops-tui (done)
|
||||
|
||||
## lot-2-domaines
|
||||
- [x] auth-chat (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-2-domaines/auth-chat.csv
|
||||
- [x] personas (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-2-domaines/personas.csv
|
||||
- [x] node-engine (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-2-domaines/node-engine.csv
|
||||
|
||||
- [x] auth-chat (done)
|
||||
- [x] personas (done)
|
||||
- [x] node-engine (done)
|
||||
|
||||
## lot-3-surfaces
|
||||
- [x] web-shell (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-3-surfaces/web-shell.csv
|
||||
- [x] persona-ui (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-3-surfaces/persona-ui.csv
|
||||
- [x] engine-ui (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-3-surfaces/engine-ui.csv
|
||||
|
||||
- [x] web-shell (done)
|
||||
- [x] persona-ui (done)
|
||||
- [x] engine-ui (done)
|
||||
|
||||
## lot-4-bascule
|
||||
- [x] migration (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-4-bascule/migration.csv
|
||||
- [x] parity (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-4-bascule/parity.csv
|
||||
- [x] cutover (done) | out: /Users/electron/Documents/Projets_Creatifs/kxkm_clown/ops/v2/outputs/lot-4-bascule/cutover.csv
|
||||
|
||||
- [x] migration (done)
|
||||
- [x] parity (done)
|
||||
- [x] cutover (done)
|
||||
|
||||
## lot-5-production
|
||||
|
||||
- [x] training-adapters (done)
|
||||
- [x] sandboxing (done)
|
||||
- [x] tests (done)
|
||||
- [x] turborepo (done)
|
||||
- [x] ci-cd (done)
|
||||
|
||||
## lot-6-consolidation
|
||||
|
||||
- [x] deep-analyse (done)
|
||||
- [x] security-fixes (done)
|
||||
- [x] feature-parity (done)
|
||||
- [x] deploy (done)
|
||||
|
||||
## lot-7-training
|
||||
|
||||
- [x] pytorch-unsloth (done)
|
||||
- [x] dpo-pipeline (done)
|
||||
- [x] autoresearch (done)
|
||||
- [x] ollama-import (done)
|
||||
|
||||
## lot-8-multimodal
|
||||
|
||||
- [x] rag-local (done)
|
||||
- [x] stt-whisper (done)
|
||||
- [x] tts-piper (done)
|
||||
- [x] vision-analysis (done)
|
||||
- [x] pdf-extraction (done)
|
||||
- [x] web-search (done)
|
||||
- [x] persona-memory (done)
|
||||
|
||||
## lot-9-chat-avance
|
||||
|
||||
- [x] voice-chat (done)
|
||||
- [x] inter-persona (done)
|
||||
- [x] multi-channel (done)
|
||||
- [x] analytics (done)
|
||||
- [x] 26-personas (done)
|
||||
|
||||
## lot-10-generation
|
||||
|
||||
- [x] comfyui (done)
|
||||
- [x] sherlock-picasso (done)
|
||||
- [x] model-diversification (done)
|
||||
- [x] context-750mb (done)
|
||||
|
||||
## lot-11-mcp-personas
|
||||
|
||||
- [x] mcp-tool-calling (done)
|
||||
- [x] pipeline-finetune (done)
|
||||
|
||||
## lot-12-deep-audit
|
||||
|
||||
- [x] deep-audit-tui (done) | ops/v2/deep-audit.js
|
||||
- [x] oss-watch-enriched (done) | docs/OSS_WATCH_2026-03-16.md
|
||||
- [x] mermaid-diagrams (done) | docs/ARCHITECTURE.md
|
||||
- [x] agents-matrix (done) | docs/AGENTS.md
|
||||
- [x] plan-todo-consolidation (done) | PLAN.md + TODO.md
|
||||
- [ ] deep-analyse-agents (in-progress) | 5 agents paralleles
|
||||
- [ ] refactor-ws-chat (pending) | 1449 LOC → 4 modules
|
||||
- [ ] refactor-app-ts (pending) | 1292 LOC → routes + middleware
|
||||
- [ ] searxng-docker (pending) | docker-compose service
|
||||
- [ ] mineru-docling (pending) | remplacer pdf-parse
|
||||
- [ ] discord-pharmacius (pending) | bot Discord orchestrateur
|
||||
- [ ] bge-m3-spike (pending) | upgrade embeddings RAG
|
||||
|
||||
## lot-13-voice-mcp
|
||||
|
||||
- [ ] xtts-v2-voice-cloning (planned)
|
||||
- [ ] llmrtc-webrtc (planned)
|
||||
- [ ] mcp-sdk-integration (planned)
|
||||
- [ ] pcl-opencharacter (planned)
|
||||
|
||||
## lot-14-music-creative
|
||||
|
||||
- [ ] ace-step-production (planned)
|
||||
- [ ] compose-command (planned)
|
||||
- [ ] flux2-comfyui (planned)
|
||||
- [ ] a2a-protocol (planned)
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
#!/usr/bin/env node
|
||||
// Deep Audit TUI — analyse code quality, security, perf across the monorepo
|
||||
// Usage: node ops/v2/deep-audit.js [--json] [--watch] [--fix] [--help]
|
||||
//
|
||||
// Checks:
|
||||
// 1. TypeScript compilation (apps + packages)
|
||||
// 2. Security patterns (path traversal, injection, SSRF)
|
||||
// 3. Performance anti-patterns (sync I/O, missing cache, unbounded)
|
||||
// 4. Dead code detection (unused exports)
|
||||
// 5. Dependency health (outdated, duplicates)
|
||||
// 6. Test coverage summary
|
||||
// 7. File size / complexity metrics
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync, exec } = require("child_process");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ANSI helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NO_COLOR = !!process.env.NO_COLOR;
|
||||
const c = {
|
||||
green: (s) => NO_COLOR ? s : `\x1b[32m${s}\x1b[0m`,
|
||||
red: (s) => NO_COLOR ? s : `\x1b[31m${s}\x1b[0m`,
|
||||
yellow: (s) => NO_COLOR ? s : `\x1b[33m${s}\x1b[0m`,
|
||||
cyan: (s) => NO_COLOR ? s : `\x1b[36m${s}\x1b[0m`,
|
||||
magenta: (s) => NO_COLOR ? s : `\x1b[35m${s}\x1b[0m`,
|
||||
bold: (s) => NO_COLOR ? s : `\x1b[1m${s}\x1b[0m`,
|
||||
dim: (s) => NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`,
|
||||
};
|
||||
|
||||
function stripAnsi(s) {
|
||||
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
function dot(ok) {
|
||||
return ok ? c.green("●") : c.red("●");
|
||||
}
|
||||
|
||||
function warn() {
|
||||
return c.yellow("▲");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
${c.bold("KXKM Deep Audit TUI")}
|
||||
|
||||
Usage: node ops/v2/deep-audit.js [options]
|
||||
|
||||
Options:
|
||||
--json Output results as JSON (pipe-friendly)
|
||||
--watch Re-run every 30 seconds
|
||||
--verbose Show detailed findings
|
||||
--help Show this help message
|
||||
|
||||
Checks:
|
||||
TypeScript compilation, security patterns, performance anti-patterns,
|
||||
dead code, dependency health, test results, file metrics.
|
||||
|
||||
Logs: ops/v2/logs/deep-audit-YYYY-MM-DD.log
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const FLAG_JSON = args.includes("--json");
|
||||
const FLAG_WATCH = args.includes("--watch");
|
||||
const FLAG_VERBOSE = args.includes("--verbose");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, "../..");
|
||||
const LOG_DIR = path.join(__dirname, "logs");
|
||||
const APPS = ["apps/api", "apps/web", "apps/worker"];
|
||||
const PACKAGES = ["packages/core", "packages/auth", "packages/chat-domain",
|
||||
"packages/persona-domain", "packages/node-engine", "packages/storage",
|
||||
"packages/ui", "packages/tui"];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ensureLogDir() {
|
||||
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function logFile() {
|
||||
const d = new Date().toISOString().slice(0, 10);
|
||||
return path.join(LOG_DIR, `deep-audit-${d}.log`);
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
const ts = new Date().toISOString();
|
||||
const line = `[${ts}] ${stripAnsi(msg)}\n`;
|
||||
fs.appendFileSync(logFile(), line, "utf-8");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Checks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function runCmd(cmd, cwd = ROOT_DIR) {
|
||||
try {
|
||||
return { ok: true, out: execSync(cmd, { cwd, encoding: "utf-8", timeout: 60_000, stdio: ["pipe", "pipe", "pipe"] }).trim() };
|
||||
} catch (e) {
|
||||
return { ok: false, out: (e.stdout || "") + (e.stderr || "") };
|
||||
}
|
||||
}
|
||||
|
||||
// 1. TypeScript compilation
|
||||
function checkTypeScript() {
|
||||
const results = [];
|
||||
for (const dir of [...APPS, ...PACKAGES]) {
|
||||
const tsconfig = path.join(ROOT_DIR, dir, "tsconfig.json");
|
||||
if (!fs.existsSync(tsconfig)) {
|
||||
results.push({ dir, status: "skip", errors: 0, detail: "no tsconfig.json" });
|
||||
continue;
|
||||
}
|
||||
const r = runCmd(`npx tsc --noEmit --pretty false 2>&1 | head -50`, path.join(ROOT_DIR, dir));
|
||||
const errCount = (r.out.match(/error TS/g) || []).length;
|
||||
results.push({ dir, status: errCount === 0 ? "ok" : "fail", errors: errCount, detail: r.out.slice(0, 500) });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// 2. Security pattern scan
|
||||
function checkSecurity() {
|
||||
const patterns = [
|
||||
{ name: "eval()", pattern: /\beval\s*\(/g, severity: "P0" },
|
||||
{ name: "child_process unsanitized", pattern: /exec\(\s*[`"'].*\$\{/g, severity: "P0" },
|
||||
{ name: "path.join user input", pattern: /path\.join\(.*req\.(params|query|body)/g, severity: "P1" },
|
||||
{ name: "innerHTML assignment", pattern: /\.innerHTML\s*=/g, severity: "P1" },
|
||||
{ name: "dangerouslySetInnerHTML", pattern: /dangerouslySetInnerHTML/g, severity: "P1" },
|
||||
{ name: "TODO security", pattern: /TODO.*secur|FIXME.*secur|HACK.*secur/gi, severity: "P2" },
|
||||
{ name: "hardcoded secret", pattern: /(password|secret|token)\s*[:=]\s*["'][^"']{8,}/gi, severity: "P1" },
|
||||
{ name: "fs.writeFileSync in handler", pattern: /writeFileSync/g, severity: "P2" },
|
||||
];
|
||||
|
||||
const findings = [];
|
||||
const dirs = [...APPS, ...PACKAGES].map(d => path.join(ROOT_DIR, d, "src"));
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
const files = getAllFiles(dir, [".ts", ".tsx", ".js"]);
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const relPath = path.relative(ROOT_DIR, file);
|
||||
for (const p of patterns) {
|
||||
if (
|
||||
p.name === "hardcoded secret" &&
|
||||
(/(^|\/)tests?\//.test(relPath) || /\.test\.[tj]sx?$/.test(relPath))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let match;
|
||||
const regex = new RegExp(p.pattern.source, p.pattern.flags);
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
const snippet = lines[lineNum - 1]?.trim().slice(0, 120) || "";
|
||||
|
||||
if (p.name === "hardcoded secret" && snippet.includes("process.env.")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
findings.push({
|
||||
severity: p.severity,
|
||||
pattern: p.name,
|
||||
file: relPath,
|
||||
line: lineNum,
|
||||
snippet,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return findings.sort((a, b) => a.severity.localeCompare(b.severity));
|
||||
}
|
||||
|
||||
// 3. Performance anti-patterns
|
||||
function checkPerformance() {
|
||||
const patterns = [
|
||||
{ name: "readFileSync in non-init", pattern: /readFileSync/g, severity: "P1" },
|
||||
{ name: "JSON.parse without try/catch", pattern: /JSON\.parse\(/g, severity: "P2", checkCatch: true },
|
||||
{ name: "unbounded array push", pattern: /\.push\([^)]+\).*(?:while|for)\b/g, severity: "P2" },
|
||||
{ name: "console.log in production", pattern: /console\.log\(/g, severity: "P2" },
|
||||
];
|
||||
|
||||
const findings = [];
|
||||
const dirs = APPS.map(d => path.join(ROOT_DIR, d, "src"));
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
const files = getAllFiles(dir, [".ts", ".tsx"]);
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
for (const p of patterns) {
|
||||
const regex = new RegExp(p.pattern.source, p.pattern.flags);
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||||
|
||||
if (p.name === "JSON.parse without try/catch" && hasNearbyTry(lines, lineNum)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
findings.push({
|
||||
severity: p.severity,
|
||||
pattern: p.name,
|
||||
file: path.relative(ROOT_DIR, file),
|
||||
line: lineNum,
|
||||
snippet: lines[lineNum - 1]?.trim().slice(0, 120) || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function hasNearbyTry(lines, lineNum) {
|
||||
const start = Math.max(0, lineNum - 8);
|
||||
for (let i = lineNum - 1; i >= start; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
if (/^try\b/.test(line) || /\btry\s*\{/.test(line)) return true;
|
||||
if (/^function\b|=>\s*\{|^for\b|^while\b/.test(line)) break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. File metrics
|
||||
function checkMetrics() {
|
||||
const metrics = [];
|
||||
const dirs = [...APPS, ...PACKAGES].map(d => path.join(ROOT_DIR, d, "src"));
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
const files = getAllFiles(dir, [".ts", ".tsx"]);
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const lineCount = content.split("\n").length;
|
||||
if (lineCount > 200) {
|
||||
metrics.push({
|
||||
file: path.relative(ROOT_DIR, file),
|
||||
lines: lineCount,
|
||||
sizeKB: Math.round(fs.statSync(file).size / 1024),
|
||||
flag: lineCount > 500 ? "large" : lineCount > 300 ? "medium" : "ok",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return metrics.sort((a, b) => b.lines - a.lines);
|
||||
}
|
||||
|
||||
// 5. Test results summary
|
||||
function checkTests() {
|
||||
const r = runCmd("npm run test:v2 2>&1 | tail -20");
|
||||
const passMatch = r.out.match(/(\d+)\s+pass/i);
|
||||
const failMatch = r.out.match(/(\d+)\s+fail/i);
|
||||
return {
|
||||
status: r.ok ? "ok" : "fail",
|
||||
pass: passMatch ? parseInt(passMatch[1]) : 0,
|
||||
fail: failMatch ? parseInt(failMatch[1]) : 0,
|
||||
output: r.out.slice(-500),
|
||||
};
|
||||
}
|
||||
|
||||
// 6. Dependency check
|
||||
function checkDeps() {
|
||||
const r = runCmd("npm outdated --json 2>/dev/null || true");
|
||||
try {
|
||||
const outdated = JSON.parse(r.out || "{}");
|
||||
return Object.entries(outdated).map(([pkg, info]) => ({
|
||||
package: pkg,
|
||||
current: info.current,
|
||||
wanted: info.wanted,
|
||||
latest: info.latest,
|
||||
})).slice(0, 20);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getAllFiles(dir, exts) {
|
||||
const results = [];
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== "dist") {
|
||||
results.push(...getAllFiles(full, exts));
|
||||
} else if (entry.isFile() && exts.some(ext => entry.name.endsWith(ext))) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
} catch { /* permission denied or missing */ }
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function drawBox(title, content) {
|
||||
const lines = content.split("\n");
|
||||
const maxLen = Math.max(title.length + 4, ...lines.map(l => stripAnsi(l).length));
|
||||
const w = Math.min(maxLen + 4, process.stdout.columns || 120);
|
||||
const hr = "─".repeat(w - 2);
|
||||
|
||||
console.log(`┌${hr}┐`);
|
||||
console.log(`│ ${c.bold(c.cyan(title))}${" ".repeat(Math.max(0, w - stripAnsi(title).length - 4))} │`);
|
||||
console.log(`├${hr}┤`);
|
||||
for (const line of lines) {
|
||||
const pad = Math.max(0, w - stripAnsi(line).length - 4);
|
||||
console.log(`│ ${line}${" ".repeat(pad)} │`);
|
||||
}
|
||||
console.log(`└${hr}┘`);
|
||||
}
|
||||
|
||||
function formatSeverity(s) {
|
||||
if (s === "P0") return c.red(c.bold("P0"));
|
||||
if (s === "P1") return c.yellow("P1");
|
||||
return c.dim("P2");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function run() {
|
||||
ensureLogDir();
|
||||
const startTime = Date.now();
|
||||
const ts = new Date().toISOString();
|
||||
log(`=== Deep Audit started at ${ts} ===`);
|
||||
|
||||
if (!FLAG_JSON) {
|
||||
console.clear();
|
||||
console.log(c.bold(c.magenta("\n ╔══════════════════════════════════╗")));
|
||||
console.log(c.bold(c.magenta(" ║ KXKM Deep Audit — 3615-KXKM ║")));
|
||||
console.log(c.bold(c.magenta(" ╚══════════════════════════════════╝\n")));
|
||||
}
|
||||
|
||||
const results = {};
|
||||
|
||||
// --- Security scan ---
|
||||
if (!FLAG_JSON) process.stdout.write(c.dim(" Scanning security patterns... "));
|
||||
const secFindings = checkSecurity();
|
||||
results.security = secFindings;
|
||||
const secP0 = secFindings.filter(f => f.severity === "P0").length;
|
||||
const secP1 = secFindings.filter(f => f.severity === "P1").length;
|
||||
if (!FLAG_JSON) {
|
||||
console.log(`${secP0 === 0 ? dot(true) : dot(false)} ${secP0} P0, ${secP1} P1, ${secFindings.length} total`);
|
||||
log(`Security: ${secP0} P0, ${secP1} P1, ${secFindings.length} total`);
|
||||
if (FLAG_VERBOSE && secFindings.length > 0) {
|
||||
const lines = secFindings.slice(0, 15).map(f =>
|
||||
` ${formatSeverity(f.severity)} ${c.dim(f.file)}:${f.line} — ${f.pattern}`
|
||||
).join("\n");
|
||||
drawBox("Security Findings", lines);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Performance scan ---
|
||||
if (!FLAG_JSON) process.stdout.write(c.dim(" Scanning performance patterns... "));
|
||||
const perfFindings = checkPerformance();
|
||||
results.performance = perfFindings;
|
||||
if (!FLAG_JSON) {
|
||||
console.log(`${warn()} ${perfFindings.length} findings`);
|
||||
log(`Performance: ${perfFindings.length} findings`);
|
||||
}
|
||||
|
||||
// --- File metrics ---
|
||||
if (!FLAG_JSON) process.stdout.write(c.dim(" Computing file metrics... "));
|
||||
const metrics = checkMetrics();
|
||||
results.metrics = metrics;
|
||||
const largeFiles = metrics.filter(m => m.flag === "large").length;
|
||||
if (!FLAG_JSON) {
|
||||
console.log(`${largeFiles > 0 ? warn() : dot(true)} ${metrics.length} files >200 LOC, ${largeFiles} >500 LOC`);
|
||||
log(`Metrics: ${metrics.length} files >200 LOC, ${largeFiles} >500 LOC`);
|
||||
if (FLAG_VERBOSE && metrics.length > 0) {
|
||||
const lines = metrics.slice(0, 10).map(m =>
|
||||
` ${m.flag === "large" ? c.red("●") : c.yellow("●")} ${c.dim(m.file)} — ${m.lines} lines (${m.sizeKB} KB)`
|
||||
).join("\n");
|
||||
drawBox("Largest Files", lines);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Deps ---
|
||||
if (!FLAG_JSON) process.stdout.write(c.dim(" Checking dependencies... "));
|
||||
const deps = checkDeps();
|
||||
results.deps = deps;
|
||||
if (!FLAG_JSON) {
|
||||
console.log(`${deps.length > 5 ? warn() : dot(true)} ${deps.length} outdated`);
|
||||
log(`Dependencies: ${deps.length} outdated`);
|
||||
}
|
||||
|
||||
// --- TypeScript ---
|
||||
if (!FLAG_JSON) process.stdout.write(c.dim(" TypeScript compilation... "));
|
||||
const tsResults = checkTypeScript();
|
||||
results.typescript = tsResults;
|
||||
const tsErrors = tsResults.reduce((sum, r) => sum + r.errors, 0);
|
||||
if (!FLAG_JSON) {
|
||||
console.log(`${tsErrors === 0 ? dot(true) : dot(false)} ${tsErrors} errors across ${tsResults.length} packages`);
|
||||
log(`TypeScript: ${tsErrors} errors`);
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
|
||||
if (FLAG_JSON) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
} else {
|
||||
console.log("");
|
||||
const summaryLines = [
|
||||
`${c.bold("Security")}: ${secP0 === 0 ? c.green("0 P0") : c.red(`${secP0} P0`)} ${secP1} P1 ${secFindings.length} total`,
|
||||
`${c.bold("Performance")}: ${perfFindings.length} anti-patterns detected`,
|
||||
`${c.bold("Complexity")}: ${largeFiles} files >500 LOC, ${metrics.length} files >200 LOC`,
|
||||
`${c.bold("TypeScript")}: ${tsErrors === 0 ? c.green("0 errors") : c.red(`${tsErrors} errors`)}`,
|
||||
`${c.bold("Deps")}: ${deps.length} outdated packages`,
|
||||
``,
|
||||
`${c.dim(`Completed in ${elapsed}s — log: ${path.relative(ROOT_DIR, logFile())}`)}`,
|
||||
].join("\n");
|
||||
drawBox("Audit Summary", summaryLines);
|
||||
}
|
||||
|
||||
log(`=== Deep Audit completed in ${elapsed}s ===`);
|
||||
|
||||
// Clean up old logs (keep 7 days)
|
||||
cleanOldLogs(7);
|
||||
}
|
||||
|
||||
function cleanOldLogs(maxDays) {
|
||||
try {
|
||||
const files = fs.readdirSync(LOG_DIR);
|
||||
const cutoff = Date.now() - maxDays * 86400000;
|
||||
for (const file of files) {
|
||||
if (!file.startsWith("deep-audit-")) continue;
|
||||
const filePath = path.join(LOG_DIR, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.mtimeMs < cutoff) {
|
||||
fs.unlinkSync(filePath);
|
||||
log(`Cleaned old log: ${file}`);
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (FLAG_WATCH) {
|
||||
const interval = 30_000;
|
||||
run();
|
||||
setInterval(run, interval);
|
||||
} else {
|
||||
run().catch(err => {
|
||||
console.error(c.red(`Fatal: ${err.message}`));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -58,8 +58,13 @@ export async function verifyPassword(password: string, hash: string): Promise<bo
|
||||
if (!salt || !storedHex) return false;
|
||||
const storedKey = Buffer.from(storedHex, "hex");
|
||||
const derived = await scryptAsync(password, salt, SCRYPT_KEYLEN);
|
||||
if (storedKey.length !== derived.length) return false;
|
||||
return timingSafeEqual(storedKey, derived);
|
||||
// Pad to equal length to prevent timing leak on corrupted stored hashes
|
||||
const maxLen = Math.max(storedKey.length, derived.length);
|
||||
const a = Buffer.alloc(maxLen);
|
||||
const b = Buffer.alloc(maxLen);
|
||||
storedKey.copy(a);
|
||||
derived.copy(b);
|
||||
return storedKey.length === derived.length && timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -71,7 +71,10 @@ export function createIsoTimestamp(date = new Date()): string {
|
||||
}
|
||||
|
||||
export function createId(prefix: string): string {
|
||||
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
const bytes = globalThis.crypto?.getRandomValues
|
||||
? globalThis.crypto.getRandomValues(new Uint8Array(8))
|
||||
: require("node:crypto").randomBytes(8);
|
||||
return `${prefix}_${Buffer.from(bytes).toString("hex")}`;
|
||||
}
|
||||
|
||||
export function isUserRole(value: string): value is UserRole {
|
||||
|
||||
@@ -621,10 +621,15 @@ export function parsePharmaciusResponse(raw: string, personaId: string): Persona
|
||||
*/
|
||||
export function applyPatches(persona: PersonaRecord, patches: PersonaPatch[]): PersonaRecord {
|
||||
const result = { ...persona } as Record<string, unknown> & PersonaRecord;
|
||||
const allowedFields = new Set<string>(["name", "model", "summary", "editable", "enabled"]);
|
||||
|
||||
for (const patch of patches) {
|
||||
if (patch.field in persona) {
|
||||
result[patch.field] = patch.after;
|
||||
if (allowedFields.has(patch.field) && patch.field in persona) {
|
||||
// Type guard: only apply if value type matches existing field type
|
||||
const existing = (persona as unknown as Record<string, unknown>)[patch.field];
|
||||
if (existing === undefined || typeof patch.after === typeof existing) {
|
||||
result[patch.field] = patch.after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const scope = process.argv[2] || "unknown";
|
||||
const action = process.argv[3] || "status";
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
scope,
|
||||
action,
|
||||
message: "V2 scaffold present. Real toolchain wiring is tracked in TODO.md.",
|
||||
}));
|
||||
Reference in New Issue
Block a user