feat: retro Claude ekip konsolunu kur
This commit is contained in:
15
server/bootstrapPrompt.js
vendored
Normal file
15
server/bootstrapPrompt.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export function buildBootstrapPrompt() {
|
||||
return [
|
||||
'Team agent modunu aktif et.',
|
||||
'Team icerisindeki roller -> Team Lead: "Mazlum" (erkek), Frontend Developer: "Berkecan" (erkek), Backend Developer: "Simsar" (erkek), UI/UX Designer: "Aybuke" (disi), iOS Developer: "Ive" (erkek) ve takim uyelerine kahveleri getirmesi icin Trainee: "Irgatov" (erkek).',
|
||||
"Bu takim yapisini aynen koru.",
|
||||
"Takim ici tum mesajlarda konusan kisi zorunlu olarak ad etiketiyle baslasin.",
|
||||
"Her cevap yalnizca su formatla baslasin: `Mazlum:` veya `Berkecan:` veya `Simsar:` veya `Aybuke:` veya `Ive:` veya `Irgatov:`.",
|
||||
"Etiketsiz cevap verme. `Ben`, `Team Lead`, `Frontend Developer`, `UI/UX Designer`, `biz`, `takim olarak` gibi baslangiclar kullanma.",
|
||||
"Gecerli ornek: `Mazlum: Buradayim.` Gecersiz ornek: `Buradayim.`",
|
||||
"Kullanici tek bir kisiye seslenirse sadece o kisi cevap versin ve cevabi kendi ad etiketiyle baslatsin.",
|
||||
"Kullanici tum takima veya genel bir goreve seslenirse once `Mazlum:` cevap versin. Gerekirse digerleri ayri satirlarda kendi ad etiketiyle devam etsin.",
|
||||
"Her ekip uyesi her mesajda kendi sabit adini kullanir, isim degistirmez.",
|
||||
"Ilk cevap olarak yalnizca takimin hazir oldugunu ve rollerin aktiflestigini bildir. Bu ilk cevap da `Mazlum:` ile baslasin."
|
||||
].join(" ");
|
||||
}
|
||||
94
server/config.js
Normal file
94
server/config.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
||||
|
||||
function toBool(value, fallback = false) {
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return String(value).toLowerCase() === "true";
|
||||
}
|
||||
|
||||
export function getActiveApiKey() {
|
||||
const activeKey = (process.env.ACTIVE_KEY ?? "pro").toLowerCase();
|
||||
if (activeKey === "lite") {
|
||||
return process.env.API_KEY_LITE ?? "";
|
||||
}
|
||||
|
||||
return process.env.API_KEY_PRO ?? "";
|
||||
}
|
||||
|
||||
export function getRuntimeConfig() {
|
||||
const claudeBin = resolveClaudeBinary(process.env.CLAUDE_BIN ?? "claude");
|
||||
return {
|
||||
port: Number(process.env.PORT ?? 3001),
|
||||
nodeEnv: process.env.NODE_ENV ?? "development",
|
||||
claudeBin,
|
||||
shell: process.env.CLAUDE_SHELL ?? "/bin/zsh",
|
||||
workspaceDir: process.env.CLAUDE_WORKSPACE_DIR
|
||||
? path.resolve(process.env.CLAUDE_WORKSPACE_DIR)
|
||||
: process.cwd(),
|
||||
anthropicBaseUrl: process.env.ANTHROPIC_BASE_URL ?? "",
|
||||
anthropicModel: process.env.ANTHROPIC_MODEL ?? "",
|
||||
activeKey: (process.env.ACTIVE_KEY ?? "pro").toLowerCase(),
|
||||
claudeArgs: process.env.CLAUDE_ARGS?.trim() ? process.env.CLAUDE_ARGS.trim().split(/\s+/) : ["--dangerously-skip-permissions"],
|
||||
watchLogLimit: Number(process.env.WATCH_LOG_LIMIT ?? 400),
|
||||
chatChunkLimit: Number(process.env.CHAT_CHUNK_LIMIT ?? 2000),
|
||||
logToConsole: toBool(process.env.LOG_TO_CONSOLE, true)
|
||||
};
|
||||
}
|
||||
|
||||
function resolveClaudeBinary(rawValue) {
|
||||
const candidates = [];
|
||||
|
||||
if (rawValue) {
|
||||
candidates.push(rawValue);
|
||||
}
|
||||
|
||||
candidates.push("/Users/wisecolt-macmini/.local/bin/claude");
|
||||
candidates.push("/usr/local/bin/claude");
|
||||
candidates.push("/opt/homebrew/bin/claude");
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (path.isAbsolute(candidate) && fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const pathEntries = String(process.env.PATH ?? "").split(path.delimiter);
|
||||
for (const entry of pathEntries) {
|
||||
const candidate = path.join(entry, rawValue);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
export function getClaudeEnv(config) {
|
||||
const cleanEnv = { ...process.env };
|
||||
delete cleanEnv.ANTHROPIC_AUTH_TOKEN;
|
||||
|
||||
return {
|
||||
...cleanEnv,
|
||||
ANTHROPIC_API_KEY: getActiveApiKey(),
|
||||
ANTHROPIC_BASE_URL: config.anthropicBaseUrl,
|
||||
ANTHROPIC_MODEL: config.anthropicModel,
|
||||
TERM: "xterm-256color",
|
||||
COLORTERM: "truecolor"
|
||||
};
|
||||
}
|
||||
|
||||
export function getPublicRuntimeConfig(config) {
|
||||
return {
|
||||
claudeBin: config.claudeBin,
|
||||
anthropicBaseUrl: config.anthropicBaseUrl,
|
||||
anthropicModel: config.anthropicModel,
|
||||
activeKey: config.activeKey,
|
||||
workspaceDir: config.workspaceDir
|
||||
};
|
||||
}
|
||||
51
server/index.js
Normal file
51
server/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import express from "express";
|
||||
import http from "http";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { Server } from "socket.io";
|
||||
import { getPublicRuntimeConfig, getRuntimeConfig } from "./config.js";
|
||||
import { SessionManager } from "./sessionManager.js";
|
||||
import { registerSocketHandlers } from "./socketHandlers.js";
|
||||
|
||||
const config = getRuntimeConfig();
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: "*"
|
||||
}
|
||||
});
|
||||
|
||||
const sessionManager = new SessionManager({ io, config });
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const webDistPath = path.resolve(__dirname, "../web/dist");
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.json({
|
||||
ok: true,
|
||||
runtime: getPublicRuntimeConfig(config),
|
||||
session: sessionManager.getState()
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/session/state", (req, res) => {
|
||||
res.json({
|
||||
state: sessionManager.getState(),
|
||||
logs: sessionManager.getLogSnapshot(),
|
||||
chat: sessionManager.getChatSnapshot()
|
||||
});
|
||||
});
|
||||
|
||||
if (config.nodeEnv === "production") {
|
||||
app.use(express.static(webDistPath));
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(webDistPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
registerSocketHandlers(io, sessionManager);
|
||||
|
||||
server.listen(config.port, () => {
|
||||
console.log(`Retro console server listening on http://localhost:${config.port}`);
|
||||
});
|
||||
40
server/logService.js
Normal file
40
server/logService.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export class LogService {
|
||||
constructor(limit = 400, logger = console) {
|
||||
this.limit = limit;
|
||||
this.logger = logger;
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
createEntry(type, message, meta = {}) {
|
||||
return {
|
||||
id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
type,
|
||||
message,
|
||||
meta,
|
||||
ts: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
push(type, message, meta = {}) {
|
||||
const entry = this.createEntry(type, message, meta);
|
||||
this.entries.push(entry);
|
||||
|
||||
if (this.entries.length > this.limit) {
|
||||
this.entries.splice(0, this.entries.length - this.limit);
|
||||
}
|
||||
|
||||
if (this.logger && type !== "output") {
|
||||
this.logger.info(`[${entry.type}] ${entry.message}`);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
return [...this.entries];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.entries = [];
|
||||
}
|
||||
}
|
||||
167
server/ptyService.js
Normal file
167
server/ptyService.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const TYPE_CHUNK_SIZE = 18;
|
||||
const TYPE_CHUNK_DELAY_MS = 22;
|
||||
|
||||
function shellEscape(value) {
|
||||
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function buildLaunchCommand(command, args, env) {
|
||||
const exports = Object.entries(env)
|
||||
.filter(([, value]) => value != null && value !== "")
|
||||
.map(([key, value]) => `export ${key}=${shellEscape(value)}`)
|
||||
.join("; ");
|
||||
|
||||
const commandPart = [command, ...args].map((part) => shellEscape(part)).join(" ");
|
||||
return `${exports}; exec ${commandPart}`;
|
||||
}
|
||||
|
||||
async function runTmux(args) {
|
||||
return execFileAsync("/opt/homebrew/bin/tmux", args);
|
||||
}
|
||||
|
||||
async function typeLikeHuman(sessionName, text) {
|
||||
for (let index = 0; index < text.length; index += TYPE_CHUNK_SIZE) {
|
||||
const chunk = text.slice(index, index + TYPE_CHUNK_SIZE);
|
||||
if (chunk) {
|
||||
await runTmux(["send-keys", "-t", sessionName, "-l", chunk]);
|
||||
await wait(TYPE_CHUNK_DELAY_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PtyService {
|
||||
constructor({ cwd, env }) {
|
||||
this.cwd = cwd;
|
||||
this.env = env;
|
||||
this.sessionName = null;
|
||||
this.pollTimer = null;
|
||||
this.lastSnapshot = "";
|
||||
this.onData = null;
|
||||
this.onExit = null;
|
||||
}
|
||||
|
||||
async start({ command, args = [], onData, onExit }) {
|
||||
if (this.sessionName) {
|
||||
throw new Error("PTY session is already running");
|
||||
}
|
||||
|
||||
this.onData = onData;
|
||||
this.onExit = onExit;
|
||||
this.lastSnapshot = "";
|
||||
this.sessionName = `retro_claude_${Date.now()}`;
|
||||
const launchCommand = buildLaunchCommand(command, args, this.env);
|
||||
|
||||
await runTmux([
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
this.sessionName,
|
||||
"-c",
|
||||
this.cwd,
|
||||
"/bin/zsh",
|
||||
"-lc",
|
||||
launchCommand
|
||||
]);
|
||||
|
||||
this.startPolling();
|
||||
}
|
||||
|
||||
async write(input) {
|
||||
if (!this.sessionName) {
|
||||
throw new Error("No active PTY session");
|
||||
}
|
||||
|
||||
const normalized = String(input ?? "");
|
||||
const chunks = normalized.split(/\r\n|\r|\n/);
|
||||
const shouldPressEnterAtEnd = /[\r\n]$/.test(normalized);
|
||||
|
||||
for (let index = 0; index < chunks.length; index += 1) {
|
||||
const chunk = chunks[index];
|
||||
if (chunk) {
|
||||
await typeLikeHuman(this.sessionName, chunk);
|
||||
}
|
||||
|
||||
if (index < chunks.length - 1) {
|
||||
await runTmux(["send-keys", "-t", this.sessionName, "Enter"]);
|
||||
await wait(35);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPressEnterAtEnd) {
|
||||
await wait(60);
|
||||
await runTmux(["send-keys", "-t", this.sessionName, "Enter"]);
|
||||
}
|
||||
}
|
||||
|
||||
resize(cols, rows) {
|
||||
void cols;
|
||||
void rows;
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.sessionName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionName = this.sessionName;
|
||||
this.stopPolling();
|
||||
this.sessionName = null;
|
||||
this.lastSnapshot = "";
|
||||
await runTmux(["kill-session", "-t", sessionName]).catch(() => {});
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return Boolean(this.sessionName);
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
this.stopPolling();
|
||||
this.pollTimer = setInterval(async () => {
|
||||
if (!this.sessionName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await runTmux(["capture-pane", "-p", "-t", this.sessionName, "-S", "-200"]);
|
||||
const snapshot = stdout ?? "";
|
||||
if (!snapshot || snapshot === this.lastSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = snapshot.startsWith(this.lastSnapshot)
|
||||
? snapshot.slice(this.lastSnapshot.length)
|
||||
: snapshot;
|
||||
|
||||
this.lastSnapshot = snapshot;
|
||||
this.onData?.(payload);
|
||||
} catch (error) {
|
||||
const message = String(error.stderr ?? error.message ?? "");
|
||||
if (message.includes("can't find session")) {
|
||||
const previous = this.sessionName;
|
||||
this.stopPolling();
|
||||
this.sessionName = null;
|
||||
this.lastSnapshot = "";
|
||||
this.onExit?.({
|
||||
exitCode: 0,
|
||||
signal: 0,
|
||||
error: previous ? undefined : "tmux session missing"
|
||||
});
|
||||
} else {
|
||||
this.onData?.(`\n[tmux-error] ${message}\n`);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
234
server/sessionManager.js
Normal file
234
server/sessionManager.js
Normal 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;
|
||||
}
|
||||
}
|
||||
56
server/socketHandlers.js
Normal file
56
server/socketHandlers.js
Normal file
@@ -0,0 +1,56 @@
|
||||
export function registerSocketHandlers(io, sessionManager) {
|
||||
io.on("connection", (socket) => {
|
||||
socket.emit("session:state", sessionManager.getState());
|
||||
socket.emit("log:snapshot", sessionManager.getLogSnapshot());
|
||||
socket.emit("chat:snapshot", { content: sessionManager.getChatSnapshot() });
|
||||
|
||||
socket.on("session:start", async (payload, callback) => {
|
||||
try {
|
||||
await sessionManager.start();
|
||||
callback?.({ ok: true });
|
||||
} catch (error) {
|
||||
socket.emit("session:error", { message: error.message });
|
||||
callback?.({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("session:stop", async (payload, callback) => {
|
||||
try {
|
||||
await sessionManager.stop();
|
||||
callback?.({ ok: true });
|
||||
} catch (error) {
|
||||
socket.emit("session:error", { message: error.message });
|
||||
callback?.({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("team:activate", async (payload, callback) => {
|
||||
try {
|
||||
await sessionManager.activateTeam();
|
||||
callback?.({ ok: true });
|
||||
} catch (error) {
|
||||
socket.emit("session:error", { message: error.message });
|
||||
callback?.({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("prompt:send", async ({ prompt }, callback) => {
|
||||
try {
|
||||
await sessionManager.sendPrompt(prompt);
|
||||
callback?.({ ok: true });
|
||||
} catch (error) {
|
||||
socket.emit("session:error", { message: error.message });
|
||||
callback?.({ ok: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("terminal:resize", ({ cols, rows }) => {
|
||||
sessionManager.resize({ cols, rows });
|
||||
});
|
||||
|
||||
socket.on("logs:clear", (payload, callback) => {
|
||||
sessionManager.clearLogs();
|
||||
callback?.({ ok: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
31
server/teamConfig.js
Normal file
31
server/teamConfig.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const TEAM_MEMBERS = [
|
||||
{ id: "mazlum", name: "Mazlum", aliases: ["mazlum", "team lead", "lead"] },
|
||||
{ id: "berkecan", name: "Berkecan", aliases: ["berkecan", "frontend developer", "frontend"] },
|
||||
{ id: "simsar", name: "Simsar", aliases: ["simsar", "backend developer", "backend"] },
|
||||
{ id: "aybuke", name: "Aybuke", aliases: ["aybuke", "aybüke", "ui/ux designer", "designer"] },
|
||||
{ id: "ive", name: "Ive", aliases: ["ive", "ios developer", "ios"] },
|
||||
{ id: "irgatov", name: "Irgatov", aliases: ["irgatov", "trainee", "intern"] }
|
||||
];
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value ?? "")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function findMentionedMember(prompt) {
|
||||
const normalizedPrompt = normalizeText(prompt);
|
||||
|
||||
for (const member of TEAM_MEMBERS) {
|
||||
for (const alias of member.aliases) {
|
||||
if (normalizedPrompt.includes(normalizeText(alias))) {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export { TEAM_MEMBERS };
|
||||
Reference in New Issue
Block a user