import fs from "fs"; import path from "path"; import stripAnsi from "strip-ansi"; import { buildBootstrapPrompt } from "./bootstrapPrompt.js"; import { LogService } from "./logService.js"; import { PtyService } from "./ptyService.js"; import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js"; import { findMentionedMember, findMentionedMembers } from "./teamConfig.js"; function cleanChunk(value) { return stripAnsi(value).replace(/\r/g, ""); } function normalizeText(value) { return String(value ?? "") .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .trim(); } function isLikelyFollowUp(prompt) { const normalized = normalizeText(prompt); const wordCount = normalized.split(/\s+/).filter(Boolean).length; return [ wordCount <= 8, /^(evet|hayir|tamam|olur|olsun|sade|sekersiz|detaylandir|detaylandir|devam|peki|neden|nasil|biraz ac|kisalt|ornek ver)\b/i.test(normalized) ].some(Boolean); } function isBriefingRequest(prompt) { const normalized = normalizeText(prompt); return [ "brief", "briefing", "ozet", "durum raporu", "tamamlandi mi", "teslim durumu", "son durum", "rapor ver", "breef" ].some((token) => normalized.includes(token)); } function isCoordinationRequest(prompt) { const normalized = normalizeText(prompt); return [ "kendi aranizda konusun", "aranizda konusun", "toplanin", "koordine olun", "birbirinizle konusun", "tartisin", "degerlendirin", "ekipce karar verin", "kendi aralarinizda", "kendi aranizda" ].some((token) => normalized.includes(token)); } function buildGeneralPrompt(prompt) { return { mode: "general", targetMember: null, routedPrompt: `Yonlendirme notu: Bu mesaj tum ekibe yoneliktir. Once Mazlum cevap versin. Gerekirse diger ekip uyeleri kendi ad etiketiyle kisa katkilar yapsin. Kullaniciya konusurken herkes Patron diye hitap etsin. Ekip ici diyalog sadece gerekiyorsa kisa olsun. Gereksiz roleplay yapma. Sonuc net ve uygulanabilir olsun. Kullanici mesaji: ${prompt}` }; } function buildBriefingPrompt(prompt) { return { mode: "briefing", targetMember: null, routedPrompt: `Yonlendirme notu: Bu mesaj proje ozeti veya teslim briefigi gerektiriyor. Son ozet yalnizca Mazlum tarafindan verilsin. Cevap Mazlum: ile baslasin. Kullaniciya mutlaka Patron diye hitap et. Ozet duzenli, yonetsel ve net olsun. Gerekirse yapilanlar, kalan riskler ve sonraki adimlar kisaca belirtilsin. Diger ekip uyeleri yalnizca zorunluysa kisa katkida bulunsun. Kullanici mesaji: ${prompt}` }; } function buildCoordinationPrompt(prompt) { return { mode: "coordination", targetMember: null, routedPrompt: `Yonlendirme notu: Bu mesaj kisa ekip ici koordinasyon gerektiriyor. Once Mazlum durumu acsin. Gerekirse ilgili ekip uyeleri kendi ad etiketiyle kisa konussun. Ekip ici hitap kurallarini uygula: Mazlum Bey, UI Hanim, erkek ekip uyeleri arasinda gerektiginde Frontend Kanka, Backend Kanka, iOS Kanka. Diyalog kisa olsun. Ardindan net sonuc veya karar acikca verilsin. Kullaniciya donecek cerceve saygili olsun ve Patron hitabi korunsun. Kullanici mesaji: ${prompt}` }; } function buildDirectPrompt(prompt, targetMember) { if (targetMember.name === "Irgatov") { return { mode: "irgatov_direct", targetMember, routedPrompt: `Yonlendirme notu: Bu mesaj Irgatov icindir. Yalnizca Irgatov cevap versin. Cevap Irgatov: ile baslasin. Kullaniciya Patron diye hitap et. Irgatov sadece kahve, icecek, servis ve basit ofis lojistigi konularinda cevap verir. Teknik plan, kod, mimari, dosya yapisi veya teknoloji secimi hakkinda gorus bildirmez. Kullanici mesaji: ${prompt}` }; } return { mode: "direct", targetMember, routedPrompt: `Yonlendirme notu: Bu mesaj dogrudan ${targetMember.name} icindir. Yalnizca ${targetMember.name} cevap versin. Cevap mutlaka ${targetMember.name}: ile baslasin. Kullaniciya mutlaka Patron diye hitap et. Kullaniciya karsi saygili, net ve profesyonel dil kullan. Gereksiz ekip ici diyalog kurma. Gerekirse cok kisa ofis tonu kullanabilirsin ama teknik icerigi golgeleme. Kullanici mesaji: ${prompt}` }; } function buildFollowUpPrompt(prompt, lastDirectedMember = null) { if (!lastDirectedMember) { return buildGeneralPrompt(prompt); } if (lastDirectedMember.name === "Irgatov") { return buildDirectPrompt(prompt, lastDirectedMember); } return { mode: "follow_up", targetMember: lastDirectedMember, routedPrompt: `Yonlendirme notu: Bu mesaj onceki konusmanin devamidir. Mumkunse onceki hedef kisi cevap versin. Cevap mevcut baglami korusun. Kullaniciya mutlaka Patron diye hitap et. Yalnizca gerekliyse kisa cevap ver. Gereksiz yeni ekip diyalogu baslatma. Cevap ${lastDirectedMember.name}: ile baslasin. Kullanici mesaji: ${prompt}` }; } function buildRoutedPrompt(prompt, lastDirectedMember = null) { const mentionedMembers = findMentionedMembers(prompt); if (isBriefingRequest(prompt)) { return buildBriefingPrompt(prompt); } if (isCoordinationRequest(prompt)) { return buildCoordinationPrompt(prompt); } if (mentionedMembers.length === 1) { return buildDirectPrompt(prompt, mentionedMembers[0]); } if (mentionedMembers.length > 1) { return buildGeneralPrompt(prompt); } if (lastDirectedMember && isLikelyFollowUp(prompt)) { return buildFollowUpPrompt(prompt, lastDirectedMember); } return buildGeneralPrompt(prompt); } export class SessionManager { constructor({ io, config }) { this.io = io; this.config = config; this.logService = new LogService(config.watchLogLimit, config.logToConsole ? console : null); this.ptyService = null; this.chatOutput = ""; this.lastDirectedMember = null; this.currentProjectPath = null; this.state = { status: "idle", startedAt: null, teamActivated: false, lastError: null, currentProjectPath: null, runtime: getPublicRuntimeConfig(config) }; } getState() { return { ...this.state, currentProjectPath: this.currentProjectPath, runtime: getPublicRuntimeConfig(this.config) }; } getLogSnapshot() { return this.logService.snapshot(); } getChatSnapshot() { return this.chatOutput; } emitState() { this.io.emit("session:state", this.getState()); } emitLog(entry) { this.io.emit("log:entry", entry); } emitChat(chunk) { this.io.emit("chat:chunk", { chunk }); } addLog(type, message, meta = {}) { const entry = this.logService.push(type, message, meta); this.emitLog(entry); return entry; } setState(patch) { this.state = { ...this.state, ...patch }; this.emitState(); } getActiveWorkspaceDir() { return this.currentProjectPath ?? this.config.workspaceDir; } async setProjectPath(projectPath) { const resolved = projectPath ? path.resolve(projectPath) : null; const wasRunning = this.ptyService?.isRunning() ?? false; if (resolved && (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory())) { throw new Error(`Selected project path is invalid: ${resolved}`); } if (wasRunning) { await this.stop(); } this.currentProjectPath = resolved; this.lastDirectedMember = null; this.setState({ currentProjectPath: resolved, teamActivated: false }); this.addLog("system", `Current project set to ${resolved ?? "None"}`); if (wasRunning) { await this.start(); if (resolved) { await this.activateTeam(); } } } async start() { if (this.ptyService?.isRunning()) { throw new Error("Session is already running"); } this.chatOutput = ""; this.lastDirectedMember = null; this.logService.clear(); this.io.emit("chat:reset"); this.ptyService = new PtyService({ cwd: this.getActiveWorkspaceDir(), env: getClaudeEnv(this.config) }); this.setState({ status: "starting", startedAt: new Date().toISOString(), teamActivated: false, lastError: null }); this.addLog("lifecycle", `Starting Claude session in ${this.getActiveWorkspaceDir()}`); try { await this.ptyService.start({ command: this.config.claudeBin, args: this.config.claudeArgs, onData: (chunk) => this.handlePtyData(chunk), onExit: (event) => this.handlePtyExit(event) }); this.setState({ status: "running" }); this.addLog("lifecycle", `Claude process started with binary ${this.config.claudeBin}`); } catch (error) { this.setState({ status: "error", lastError: error.message }); this.addLog("error", error.message); throw error; } } async stop() { if (!this.ptyService?.isRunning()) { return; } this.addLog("lifecycle", "Stopping Claude session"); await this.ptyService.stop(); this.lastDirectedMember = null; this.setState({ status: "stopped", teamActivated: false }); } async sendPrompt(prompt) { if (!this.ptyService?.isRunning()) { throw new Error("Session is not running"); } const { routedPrompt, targetMember } = buildRoutedPrompt(prompt, this.lastDirectedMember); const input = `${routedPrompt}\r`; this.lastDirectedMember = targetMember ?? null; this.addLog("input", prompt); await this.ptyService.write(input); } async sendRawPrompt(prompt, meta = {}) { if (!this.ptyService?.isRunning()) { throw new Error("Session is not running"); } const input = `${prompt}\r`; this.addLog("input", meta.label ?? prompt, meta); await this.ptyService.write(input); } async activateTeam() { const prompt = buildBootstrapPrompt(this.currentProjectPath); this.lastDirectedMember = null; await this.sendRawPrompt(prompt, { label: "[bootstrap] Team activation prompt sent" }); this.setState({ teamActivated: true }); this.addLog("system", "Team activation prompt sent"); } resize({ cols, rows }) { this.ptyService?.resize(cols, rows); } clearLogs() { this.logService.clear(); this.io.emit("log:snapshot", []); this.addLog("system", "Watch log cleared"); } handlePtyData(chunk) { const clean = cleanChunk(chunk); if (!clean) { return; } this.chatOutput += clean; if (this.chatOutput.length > this.config.chatChunkLimit * 20) { this.chatOutput = this.chatOutput.slice(-this.config.chatChunkLimit * 20); } this.emitChat(clean); this.addLog("output", clean); } handlePtyExit(event) { const exitCode = event?.exitCode ?? 0; const signal = event?.signal ?? 0; const detail = event?.error ? `, error=${event.error}` : ""; this.addLog("lifecycle", `Claude session exited (code=${exitCode}, signal=${signal}${detail})`); this.setState({ status: "stopped", teamActivated: false }); this.ptyService = null; } }