import fs from "fs"; import path from "path"; import stripAnsi from "strip-ansi"; import { buildBootstrapPrompt } from "./bootstrapPrompt.js"; import { buildProjectSelectionPrompt } from "./bootstrapPrompt.js"; import { LogService } from "./logService.js"; import { PtyService } from "./ptyService.js"; import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js"; import { findMentionedMember } from "./teamConfig.js"; function cleanChunk(value) { return stripAnsi(value).replace(/\r/g, ""); } function isLikelyFollowUp(prompt) { const normalized = String(prompt ?? "").trim(); const wordCount = normalized.split(/\s+/).filter(Boolean).length; return [ wordCount <= 8, /^(evet|hayir|tamam|olur|olsun|sade|sekersiz|şekersiz|detaylandir|detaylandır|devam|peki|neden|nasil|nasıl)\b/i.test(normalized) ].some(Boolean); } function buildRoutedPrompt(prompt, lastDirectedMember = null) { const explicitTarget = findMentionedMember(prompt); const targetMember = explicitTarget ?? (lastDirectedMember && isLikelyFollowUp(prompt) ? lastDirectedMember : null); if (!targetMember) { return { targetMember: null, routedPrompt: `Not: Bu genel mesajdir. Once Mazlum cevap versin ve konusan herkes ad etiketi kullansin. Irgatov teknik gorev almaz; sadece kahve ve lojistik destek verir. Kullanici mesaji: ${prompt}` }; } if (targetMember.name === "Irgatov") { return { targetMember, routedPrompt: `Not: Bu mesaj Irgatov icindir. Irgatov sadece kahve, icecek, servis ve basit lojistik destek konularinda cevap versin. Teknik plan, kod, mimari veya dosya yapisi onermesin. Cevap \`Irgatov:\` ile baslasin. Kullanici mesaji: ${prompt}` }; } return { targetMember, routedPrompt: `Not: Bu mesaj ${targetMember.name} icindir. Yalnizca ${targetMember.name} cevap versin ve cevap \`${targetMember.name}:\` ile baslasin. Kullanici mesaji: ${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 }); this.addLog("system", `Current project set to ${resolved ?? "None"}`); if (wasRunning) { await this.start(); if (resolved) { await this.activateTeam(); } else { const prompt = buildProjectSelectionPrompt(this.getActiveWorkspaceDir()); await this.sendRawPrompt(prompt, { label: `[project] Switched active project to ${this.getActiveWorkspaceDir()}` }); } } } 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; } }