266 lines
7.2 KiB
JavaScript
266 lines
7.2 KiB
JavaScript
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 } 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,
|
||
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;
|
||
}
|
||
}
|