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 } 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. 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.state = { status: "idle", startedAt: null, teamActivated: false, lastError: null, runtime: getPublicRuntimeConfig(config) }; } getState() { return { ...this.state, 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(); } 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.config.workspaceDir, env: getClaudeEnv(this.config) }); this.setState({ status: "starting", startedAt: new Date().toISOString(), teamActivated: false, lastError: null }); this.addLog("lifecycle", `Starting Claude session in ${this.config.workspaceDir}`); 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.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; } }