feat: retro Claude ekip konsolunu kur

This commit is contained in:
2026-03-16 23:38:15 +03:00
parent 9294028fb2
commit 68d5c2afea
32 changed files with 5207 additions and 0 deletions

234
server/sessionManager.js Normal file
View File

@@ -0,0 +1,234 @@
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: [
"[YONLENDIRME NOTU - CEVAPTA TEKRAR ETME]",
"Bu mesaj genel bir gorev veya genel konusmadir.",
"Cevabi once Mazlum baslatsin.",
"Konusan herkes ad etiketi kullansin.",
"[KULLANICI MESAJI]",
prompt
].join("\n")
};
}
return {
targetMember,
routedPrompt: [
"[YONLENDIRME NOTU - CEVAPTA TEKRAR ETME]",
`Bu mesaj ${targetMember.name} icindir.`,
`Yalnizca ${targetMember.name} cevap versin.`,
`Cevap \`${targetMember.name}:\` ile baslasin.`,
"[KULLANICI MESAJI]",
prompt
].join("\n")
};
}
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;
}
}