UNPKG

vibechat

Version:

Chatroom for when you are bored waiting for Claude

1,426 lines (1,419 loc) 50.1 kB
#!/usr/bin/env node // main.tsx import { existsSync, readdirSync, statSync } from "node:fs"; import { mkdir, readFile, watch, writeFile } from "node:fs/promises"; import os2, { homedir } from "node:os"; import path from "node:path"; import { Amplify } from "aws-amplify"; import { events } from "aws-amplify/data"; // node_modules/chalk/source/vendor/ansi-styles/index.js var ANSI_BACKGROUND_OFFSET = 10; var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`; var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`; var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`; var styles = { modifier: { reset: [0, 0], // 21 isn't widely supported and 22 does the same thing bold: [1, 22], dim: [2, 22], italic: [3, 23], underline: [4, 24], overline: [53, 55], inverse: [7, 27], hidden: [8, 28], strikethrough: [9, 29] }, color: { black: [30, 39], red: [31, 39], green: [32, 39], yellow: [33, 39], blue: [34, 39], magenta: [35, 39], cyan: [36, 39], white: [37, 39], // Bright color blackBright: [90, 39], gray: [90, 39], // Alias of `blackBright` grey: [90, 39], // Alias of `blackBright` redBright: [91, 39], greenBright: [92, 39], yellowBright: [93, 39], blueBright: [94, 39], magentaBright: [95, 39], cyanBright: [96, 39], whiteBright: [97, 39] }, bgColor: { bgBlack: [40, 49], bgRed: [41, 49], bgGreen: [42, 49], bgYellow: [43, 49], bgBlue: [44, 49], bgMagenta: [45, 49], bgCyan: [46, 49], bgWhite: [47, 49], // Bright color bgBlackBright: [100, 49], bgGray: [100, 49], // Alias of `bgBlackBright` bgGrey: [100, 49], // Alias of `bgBlackBright` bgRedBright: [101, 49], bgGreenBright: [102, 49], bgYellowBright: [103, 49], bgBlueBright: [104, 49], bgMagentaBright: [105, 49], bgCyanBright: [106, 49], bgWhiteBright: [107, 49] } }; var modifierNames = Object.keys(styles.modifier); var foregroundColorNames = Object.keys(styles.color); var backgroundColorNames = Object.keys(styles.bgColor); var colorNames = [...foregroundColorNames, ...backgroundColorNames]; function assembleStyles() { const codes = /* @__PURE__ */ new Map(); for (const [groupName, group] of Object.entries(styles)) { for (const [styleName, style] of Object.entries(group)) { styles[styleName] = { open: `\x1B[${style[0]}m`, close: `\x1B[${style[1]}m` }; group[styleName] = styles[styleName]; codes.set(style[0], style[1]); } Object.defineProperty(styles, groupName, { value: group, enumerable: false }); } Object.defineProperty(styles, "codes", { value: codes, enumerable: false }); styles.color.close = "\x1B[39m"; styles.bgColor.close = "\x1B[49m"; styles.color.ansi = wrapAnsi16(); styles.color.ansi256 = wrapAnsi256(); styles.color.ansi16m = wrapAnsi16m(); styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET); styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET); styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET); Object.defineProperties(styles, { rgbToAnsi256: { value(red, green, blue) { if (red === green && green === blue) { if (red < 8) { return 16; } if (red > 248) { return 231; } return Math.round((red - 8) / 247 * 24) + 232; } return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5); }, enumerable: false }, hexToRgb: { value(hex) { const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); if (!matches) { return [0, 0, 0]; } let [colorString] = matches; if (colorString.length === 3) { colorString = [...colorString].map((character) => character + character).join(""); } const integer = Number.parseInt(colorString, 16); return [ /* eslint-disable no-bitwise */ integer >> 16 & 255, integer >> 8 & 255, integer & 255 /* eslint-enable no-bitwise */ ]; }, enumerable: false }, hexToAnsi256: { value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)), enumerable: false }, ansi256ToAnsi: { value(code) { if (code < 8) { return 30 + code; } if (code < 16) { return 90 + (code - 8); } let red; let green; let blue; if (code >= 232) { red = ((code - 232) * 10 + 8) / 255; green = red; blue = red; } else { code -= 16; const remainder = code % 36; red = Math.floor(code / 36) / 5; green = Math.floor(remainder / 6) / 5; blue = remainder % 6 / 5; } const value = Math.max(red, green, blue) * 2; if (value === 0) { return 30; } let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red)); if (value === 2) { result += 60; } return result; }, enumerable: false }, rgbToAnsi: { value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)), enumerable: false }, hexToAnsi: { value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)), enumerable: false } }); return styles; } var ansiStyles = assembleStyles(); var ansi_styles_default = ansiStyles; // node_modules/chalk/source/vendor/supports-color/index.js import process2 from "node:process"; import os from "node:os"; import tty from "node:tty"; function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) { const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--"; const position = argv.indexOf(prefix + flag); const terminatorPosition = argv.indexOf("--"); return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); } var { env } = process2; var flagForceColor; if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) { flagForceColor = 0; } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) { flagForceColor = 1; } function envForceColor() { if ("FORCE_COLOR" in env) { if (env.FORCE_COLOR === "true") { return 1; } if (env.FORCE_COLOR === "false") { return 0; } return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3); } } function translateLevel(level) { if (level === 0) { return false; } return { level, hasBasic: true, has256: level >= 2, has16m: level >= 3 }; } function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) { const noFlagForceColor = envForceColor(); if (noFlagForceColor !== void 0) { flagForceColor = noFlagForceColor; } const forceColor = sniffFlags ? flagForceColor : noFlagForceColor; if (forceColor === 0) { return 0; } if (sniffFlags) { if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) { return 3; } if (hasFlag("color=256")) { return 2; } } if ("TF_BUILD" in env && "AGENT_NAME" in env) { return 1; } if (haveStream && !streamIsTTY && forceColor === void 0) { return 0; } const min = forceColor || 0; if (env.TERM === "dumb") { return min; } if (process2.platform === "win32") { const osRelease = os.release().split("."); if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) { return Number(osRelease[2]) >= 14931 ? 3 : 2; } return 1; } if ("CI" in env) { if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) { return 3; } if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") { return 1; } return min; } if ("TEAMCITY_VERSION" in env) { return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; } if (env.COLORTERM === "truecolor") { return 3; } if (env.TERM === "xterm-kitty") { return 3; } if ("TERM_PROGRAM" in env) { const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10); switch (env.TERM_PROGRAM) { case "iTerm.app": { return version >= 3 ? 3 : 2; } case "Apple_Terminal": { return 2; } } } if (/-256(color)?$/i.test(env.TERM)) { return 2; } if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { return 1; } if ("COLORTERM" in env) { return 1; } return min; } function createSupportsColor(stream, options = {}) { const level = _supportsColor(stream, { streamIsTTY: stream && stream.isTTY, ...options }); return translateLevel(level); } var supportsColor = { stdout: createSupportsColor({ isTTY: tty.isatty(1) }), stderr: createSupportsColor({ isTTY: tty.isatty(2) }) }; var supports_color_default = supportsColor; // node_modules/chalk/source/utilities.js function stringReplaceAll(string, substring, replacer) { let index = string.indexOf(substring); if (index === -1) { return string; } const substringLength = substring.length; let endIndex = 0; let returnValue = ""; do { returnValue += string.slice(endIndex, index) + substring + replacer; endIndex = index + substringLength; index = string.indexOf(substring, endIndex); } while (index !== -1); returnValue += string.slice(endIndex); return returnValue; } function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) { let endIndex = 0; let returnValue = ""; do { const gotCR = string[index - 1] === "\r"; returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix; endIndex = index + 1; index = string.indexOf("\n", endIndex); } while (index !== -1); returnValue += string.slice(endIndex); return returnValue; } // node_modules/chalk/source/index.js var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default; var GENERATOR = Symbol("GENERATOR"); var STYLER = Symbol("STYLER"); var IS_EMPTY = Symbol("IS_EMPTY"); var levelMapping = [ "ansi", "ansi", "ansi256", "ansi16m" ]; var styles2 = /* @__PURE__ */ Object.create(null); var applyOptions = (object, options = {}) => { if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) { throw new Error("The `level` option should be an integer from 0 to 3"); } const colorLevel = stdoutColor ? stdoutColor.level : 0; object.level = options.level === void 0 ? colorLevel : options.level; }; var chalkFactory = (options) => { const chalk2 = (...strings) => strings.join(" "); applyOptions(chalk2, options); Object.setPrototypeOf(chalk2, createChalk.prototype); return chalk2; }; function createChalk(options) { return chalkFactory(options); } Object.setPrototypeOf(createChalk.prototype, Function.prototype); for (const [styleName, style] of Object.entries(ansi_styles_default)) { styles2[styleName] = { get() { const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]); Object.defineProperty(this, styleName, { value: builder }); return builder; } }; } styles2.visible = { get() { const builder = createBuilder(this, this[STYLER], true); Object.defineProperty(this, "visible", { value: builder }); return builder; } }; var getModelAnsi = (model, level, type, ...arguments_) => { if (model === "rgb") { if (level === "ansi16m") { return ansi_styles_default[type].ansi16m(...arguments_); } if (level === "ansi256") { return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_)); } return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_)); } if (model === "hex") { return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_)); } return ansi_styles_default[type][model](...arguments_); }; var usedModels = ["rgb", "hex", "ansi256"]; for (const model of usedModels) { styles2[model] = { get() { const { level } = this; return function(...arguments_) { const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]); return createBuilder(this, styler, this[IS_EMPTY]); }; } }; const bgModel = "bg" + model[0].toUpperCase() + model.slice(1); styles2[bgModel] = { get() { const { level } = this; return function(...arguments_) { const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]); return createBuilder(this, styler, this[IS_EMPTY]); }; } }; } var proto = Object.defineProperties(() => { }, { ...styles2, level: { enumerable: true, get() { return this[GENERATOR].level; }, set(level) { this[GENERATOR].level = level; } } }); var createStyler = (open, close, parent) => { let openAll; let closeAll; if (parent === void 0) { openAll = open; closeAll = close; } else { openAll = parent.openAll + open; closeAll = close + parent.closeAll; } return { open, close, openAll, closeAll, parent }; }; var createBuilder = (self, _styler, _isEmpty) => { const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" ")); Object.setPrototypeOf(builder, proto); builder[GENERATOR] = self; builder[STYLER] = _styler; builder[IS_EMPTY] = _isEmpty; return builder; }; var applyStyle = (self, string) => { if (self.level <= 0 || !string) { return self[IS_EMPTY] ? "" : string; } let styler = self[STYLER]; if (styler === void 0) { return string; } const { openAll, closeAll } = styler; if (string.includes("\x1B")) { while (styler !== void 0) { string = stringReplaceAll(string, styler.close, styler.open); styler = styler.parent; } } const lfIndex = string.indexOf("\n"); if (lfIndex !== -1) { string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); } return openAll + string + closeAll; }; Object.defineProperties(createChalk.prototype, styles2); var chalk = createChalk(); var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 }); var source_default = chalk; // main.tsx import { Box, measureElement, render, Static, Text, useApp, useInput, useStdout } from "ink"; import React, { useEffect, useRef, useState } from "react"; import semver from "semver"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; var TextInput = ({ value: originalValue = "", placeholder = "", focus = true, mask, showCursor = true, highlightPastedText = false, onChange, onSubmit }) => { const [state, setState] = useState({ cursorOffset: (originalValue || "").length, cursorWidth: 0 }); const { cursorOffset, cursorWidth } = state; useEffect(() => { setState((previousState) => { if (!focus || !showCursor) { return previousState; } const newValue = originalValue || ""; if (previousState.cursorOffset > newValue.length - 1) { return { cursorOffset: newValue.length, cursorWidth: 0 }; } return previousState; }); }, [originalValue, focus, showCursor]); const cursorActualWidth = highlightPastedText ? cursorWidth : 0; const value = mask ? mask.repeat(originalValue.length) : originalValue; let renderedValue = value; let renderedPlaceholder = placeholder ? source_default.grey(placeholder) : void 0; if (showCursor && focus) { renderedPlaceholder = placeholder.length > 0 ? source_default.inverse(placeholder[0]) + source_default.grey(placeholder.slice(1)) : source_default.inverse(" "); renderedValue = value.length > 0 ? "" : source_default.inverse(" "); let i = 0; for (const char of value) { renderedValue += i >= cursorOffset - cursorActualWidth && i <= cursorOffset ? source_default.inverse(char) : char; i++; } if (value.length > 0 && cursorOffset === value.length) { renderedValue += source_default.inverse(" "); } } useInput( (input, key) => { if (key.upArrow || key.downArrow || key.ctrl && input === "c" || key.tab || key.shift && key.tab) { return; } if (key.return) { if (onSubmit) { onSubmit(originalValue); } return; } let nextCursorOffset = cursorOffset; let nextValue = originalValue; let nextCursorWidth = 0; if (key.leftArrow) { if (showCursor) { nextCursorOffset--; } } else if (key.rightArrow) { if (showCursor) { nextCursorOffset++; } } else if (key.backspace || key.delete) { if (cursorOffset > 0) { nextValue = originalValue.slice(0, cursorOffset - 1) + originalValue.slice(cursorOffset, originalValue.length); nextCursorOffset--; } } else if (key.ctrl && input === "w") { const trimmed = originalValue.trimEnd(); const lastSpaceIndex = trimmed.lastIndexOf(" "); nextValue = lastSpaceIndex === -1 ? "" : originalValue.substring(0, lastSpaceIndex + 1); nextCursorOffset = nextValue.length; } else { nextValue = originalValue.slice(0, cursorOffset) + input + originalValue.slice(cursorOffset, originalValue.length); nextCursorOffset += input.length; if (input.length > 1) { nextCursorWidth = input.length; } } if (cursorOffset < 0) { nextCursorOffset = 0; } if (cursorOffset > originalValue.length) { nextCursorOffset = originalValue.length; } setState({ cursorOffset: nextCursorOffset, cursorWidth: nextCursorWidth }); if (nextValue !== originalValue) { onChange(nextValue); } }, { isActive: focus } ); return /* @__PURE__ */ jsx(Text, { children: placeholder ? value.length > 0 ? renderedValue : renderedPlaceholder : renderedValue }); }; Amplify.configure({ API: { Events: { endpoint: "https://o7zdazzaqzdzpgg5lgtyhaccoi.appsync-api.us-east-1.amazonaws.com/event", region: "us-east-1", defaultAuthMode: "lambda" } } }); var auth = { authMode: "lambda", authToken: "i-am-being-nice-not-evil" }; var CURRENT_VERSION = "0.1.4"; async function checkVersionAndGetPricing() { try { const response = await fetch( "https://4vfjm2zeo2nmmriejrlwsfakce0wadpd.lambda-url.us-east-1.on.aws/info" ); if (!response.ok) { return { pricing: null, banner: null, announce: null }; } const data = await response.json(); const minVersion = data.min_version; if (minVersion && semver.lt(CURRENT_VERSION, minVersion)) { console.log( `Please upgrade vibechat with \`npm i -g vibechat@latest\` as the current version (${CURRENT_VERSION}) is too old to connect (minimum required: ${minVersion})` ); process.exit(1); } return { pricing: data.pricing || null, banner: data.banner || null, announce: data.announce || null }; } catch (_error) { console.log("Unable to connect to vibechat server. Please check your internet connection."); process.exit(1); } } var ClaudeSessionMonitor = class { constructor(pricingData = null) { this.sessions = /* @__PURE__ */ new Map(); this.todayTokens = 0; this.todayCost = 0; this.watchers = []; this.isShuttingDown = false; this.modelPricing = /* @__PURE__ */ new Map(); this.onUpdate = null; // Callback for UI updates this.processedMessages = /* @__PURE__ */ new Set(); this.todayStart = this.getTodayStart(); this.currentDateKey = this.getCurrentDateKey(); this.claudePaths = this.getClaudePaths(); if (pricingData) { this.loadPricingData(pricingData); } } getTodayStart() { const today = /* @__PURE__ */ new Date(); today.setHours(0, 0, 0, 0); return today.getTime(); } isToday(timestamp) { const messageDate = new Date(timestamp); const today = /* @__PURE__ */ new Date(); return messageDate.getFullYear() === today.getFullYear() && messageDate.getMonth() === today.getMonth() && messageDate.getDate() === today.getDate(); } getCurrentDateKey() { const today = /* @__PURE__ */ new Date(); return `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; } loadPricingData(pricingData) { try { for (const [modelName, pricing] of Object.entries(pricingData)) { this.modelPricing.set(modelName, pricing); } } catch (error) { console.warn( `Could not load pricing data: ${error instanceof Error ? error.message : String(error)}` ); } } getClaudePaths() { const paths = []; const envPaths = (process.env.CLAUDE_CONFIG_DIR || "").trim(); if (envPaths) { const envPathList = envPaths.split(",").map((p) => p.trim()).filter((p) => p); for (const envPath of envPathList) { if (existsSync(path.join(envPath, "projects"))) { paths.push(envPath); } } } const defaultPaths = [path.join(homedir(), ".config/claude"), path.join(homedir(), ".claude")]; for (const defaultPath of defaultPaths) { if (existsSync(path.join(defaultPath, "projects"))) { paths.push(defaultPath); } } return paths; } isUuidFilename(filename) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i.test(filename); } findAllSessions() { const sessionFiles = []; for (const claudePath of this.claudePaths) { const projectsDir = path.join(claudePath, "projects"); if (!existsSync(projectsDir)) continue; try { const projectDirs = readdirSync(projectsDir); for (const projectDir of projectDirs) { const projectPath = path.join(projectsDir, projectDir); if (!statSync(projectPath).isDirectory()) continue; try { const files = readdirSync(projectPath); for (const file of files) { if (this.isUuidFilename(file)) { const sessionId = file.replace(".jsonl", ""); const filePath = path.join(projectPath, file); sessionFiles.push({ sessionId, projectPath: projectDir, filePath }); } } } catch (_error) { } } } catch (_error) { } } return sessionFiles; } async parseLastMessage(filePath) { try { const content = await readFile(filePath, "utf-8"); const lines = content.trim().split("\n").filter((line) => line.length > 0); if (lines.length === 0) return null; for (let i = lines.length - 1; i >= 0; i--) { try { const data = JSON.parse(lines[i]); return data; } catch (_parseError) { } } return null; } catch (_error) { return null; } } async parseAllMessagesForDailyCount(filePath) { try { const content = await readFile(filePath, "utf-8"); const lines = content.trim().split("\n").filter((line) => line.length > 0); const messages = []; for (const line of lines) { try { const data = JSON.parse(line); if (data?.message && data.timestamp) { if (this.isToday(data.timestamp)) { messages.push(data); } } } catch (_parseError) { } } return messages; } catch (_error) { return []; } } isActiveMessage(messageData) { if (!messageData || !messageData.message) return false; const message = messageData.message; const timestamp = new Date(messageData.timestamp).getTime(); const now = Date.now(); const fiveMinutesAgo = now - 5 * 60 * 1e3; if (timestamp < fiveMinutesAgo) return false; if (message.role === "assistant" && message.type === "message") { const hasToolCalls = message.content?.some((item) => item.type === "tool_use"); if (hasToolCalls) return true; const textContent = message.content?.find((item) => item.type === "text"); if (textContent?.text) { const text = textContent.text.trim(); if (text.startsWith("Now I'll") || text.startsWith("I'll") || text.startsWith("Now I") || text.startsWith("Now let") || text.startsWith("Finally,") || text.includes("Let me") || text.includes("I need")) { return true; } } return false; } return true; } getTokensAndCostFromMessage(messageData) { if (!messageData || !messageData.message || !messageData.message.usage) { return { tokens: 0, cost: 0 }; } const timestamp = new Date(messageData.timestamp).getTime(); if (timestamp < this.todayStart) return { tokens: 0, cost: 0 }; const usage = messageData.message.usage; const model = messageData.message.model; const tokens = { input: usage.input_tokens || 0, output: usage.output_tokens || 0, cacheCreation: usage.cache_creation_input_tokens || 0, cacheRead: usage.cache_read_input_tokens || 0 }; const totalTokens = tokens.input + tokens.output + tokens.cacheCreation + tokens.cacheRead; let cost = 0; if (model && this.modelPricing.has(model)) { const pricing = this.modelPricing.get(model); cost = tokens.input * (pricing.input_cost_per_token || 0) + tokens.output * (pricing.output_cost_per_token || 0) + tokens.cacheCreation * (pricing.cache_creation_input_token_cost || 0) + tokens.cacheRead * (pricing.cache_read_input_token_cost || 0); } return { tokens: totalTokens, cost }; } async updateSessionState(sessionId, filePath, projectPath) { const lastMessage = await this.parseLastMessage(filePath); if (!lastMessage) return; const isActive = this.isActiveMessage(lastMessage); this.sessions.set(sessionId, { status: isActive ? "ACTIVE" : "INACTIVE", lastMessage, filePath, projectPath }); if (this.onUpdate) { this.onUpdate({ activeSessions: this.getActiveSessions(), todayCost: this.todayCost, todayTokens: this.todayTokens }); } } async updateSession(sessionId, filePath, projectPath) { const newDateKey = this.getCurrentDateKey(); if (newDateKey !== this.currentDateKey) { this.currentDateKey = newDateKey; this.todayStart = this.getTodayStart(); this.todayTokens = 0; this.todayCost = 0; this.processedMessages.clear(); await this.recalculateDailyTotals(); } const lastMessage = await this.parseLastMessage(filePath); if (!lastMessage) return; const isActive = this.isActiveMessage(lastMessage); const previousSession = this.sessions.get(sessionId); const _previousStatus = previousSession ? previousSession.status : null; this.sessions.set(sessionId, { status: isActive ? "ACTIVE" : "INACTIVE", lastMessage, filePath, projectPath }); const { tokens, cost } = this.getTokensAndCostFromMessage(lastMessage); const messageId = lastMessage.message?.id; if (tokens > 0 && messageId && !this.processedMessages.has(messageId)) { this.todayTokens += tokens; this.todayCost += cost; this.processedMessages.add(messageId); } if (this.onUpdate) { this.onUpdate({ activeSessions: this.getActiveSessions(), todayCost: this.todayCost, todayTokens: this.todayTokens }); } } getActiveSessions() { return Array.from(this.sessions.values()).filter((session) => session.status === "ACTIVE").length; } async recalculateDailyTotals() { const sessionFiles = this.findAllSessions(); this.todayTokens = 0; this.todayCost = 0; this.processedMessages.clear(); const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1e3; for (const { filePath } of sessionFiles) { try { const stats = statSync(filePath); if (stats.mtime.getTime() < twentyFourHoursAgo) { continue; } } catch (error) { continue; } const allMessages = await this.parseAllMessagesForDailyCount(filePath); for (const messageData of allMessages) { const { tokens, cost } = this.getTokensAndCostFromMessage(messageData); const messageId = messageData.message?.id; if (tokens > 0 && messageId && !this.processedMessages.has(messageId)) { this.todayTokens += tokens; this.todayCost += cost; this.processedMessages.add(messageId); } } } } async initialScan() { const sessionFiles = this.findAllSessions(); const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1e3; for (const { filePath } of sessionFiles) { try { const stats = statSync(filePath); if (stats.mtime.getTime() < twentyFourHoursAgo) { continue; } } catch (error) { continue; } const allMessages = await this.parseAllMessagesForDailyCount(filePath); for (const messageData of allMessages) { const { tokens, cost } = this.getTokensAndCostFromMessage(messageData); const messageId = messageData.message?.id; if (tokens > 0 && messageId && !this.processedMessages.has(messageId)) { this.todayTokens += tokens; this.todayCost += cost; this.processedMessages.add(messageId); } } } for (const { sessionId, filePath, projectPath } of sessionFiles) { await this.updateSessionState(sessionId, filePath, projectPath); } } async start() { if (this.claudePaths.length === 0) { throw new Error( "No Claude data directories found. Make sure Claude Code has been used at least once." ); } await this.initialScan(); for (const claudePath of this.claudePaths) { const projectsDir = path.join(claudePath, "projects"); this.watchDirectory(projectsDir); } } async watchDirectory(dirPath) { if (!existsSync(dirPath) || this.isShuttingDown) return; try { const watcher = watch(dirPath, { recursive: true }); this.watchers.push({ path: dirPath, watcher }); (async () => { try { for await (const event of watcher) { if (this.isShuttingDown) break; if (event.filename?.endsWith(".jsonl") && this.isUuidFilename(path.basename(event.filename))) { const fullPath = path.join(dirPath, event.filename); await this.handleFileChange(fullPath); } } } catch (_error) { } })(); } catch (_error) { } } async handleFileChange(filePath) { if (!existsSync(filePath) || this.isShuttingDown) return; try { const filename = path.basename(filePath); const sessionId = filename.replace(".jsonl", ""); const projectPath = path.basename(path.dirname(filePath)); await this.updateSession(sessionId, filePath, projectPath); } catch (_error) { } } stop() { this.isShuttingDown = true; } }; var getConfigPath = () => { const configDir = path.join(homedir(), ".config"); if (existsSync(configDir)) { return path.join(configDir, "vibechat.json"); } return null; }; var saveSettings = async (settings) => { const configPath = getConfigPath(); if (!configPath) return; try { const configDir = path.dirname(configPath); if (!existsSync(configDir)) { await mkdir(configDir, { recursive: true }); } await writeFile(configPath, JSON.stringify(settings, null, 2)); } catch (_error) { } }; var loadSettings = async () => { const configPath = getConfigPath(); if (!configPath || !existsSync(configPath)) { return {}; } try { const content = await readFile(configPath, "utf-8"); return JSON.parse(content); } catch (_error) { return {}; } }; var VibeChatLogo = ({ bannerText }) => /* @__PURE__ */ jsxs(Box, { marginTop: 2, marginBottom: 2, flexDirection: "column", alignItems: "center", children: [ /* @__PURE__ */ jsxs(Text, { color: "magenta", bold: true, children: [ `\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 `, `\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D `, `\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 `, `\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D `, ` \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 `, ` \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D `, ` ` ] }), /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [ ` \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 `, `\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D `, `\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 `, `\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 `, `\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 `, ` \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ` ] }), /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: bannerText || "https://github.com/antimatter15/vibechat" }) ] }); var ClaudeMessages = [ "Claude isn't Clauding right now. Go tell him to do something to access the chatroom.", "No Claude, no chat! Boot up Claude Code to get this conversation started.", "Claude is currently in sleep mode. Execute Claude Code to wake up the chatroom!", "Looks like Claude wandered off again. Fire up Claude Code to open the chatroom doors!", "Claude has gone silent! Summon him back with Claude Code to enter the sacred chatroom!", "Idle Claude detected. Maintain active Claude Code session for chatroom entry!", "The chatroom requires active Claude energy. Keep Claude Code clauding to get inside!", "Claude has gone into hibernation mode. Only a working Claude Code can wake the chatroom!", "Claude's gone offline! Keep Claude Code busy to unlock the chatroom access!", "Warning: Claude is slacking off. Put Claude Code to work to enter the chatroom!", "Claude stopped thinking! Get Claude Code processing to open the chatroom!", "No active Claude detected. Keep Claude Code working to join the chatroom!", "Claude went AFK! Get Claude Code clauding to unlock the chatroom doors!", "The chatroom is waiting for Claude! Get Claude Code processing to join the fun!", "The chatroom craves Claude energy! Get Claude Code clauding to get in!", "No Claude brain activity! Get Claude Code thinking to open the chatroom doors!", "Claude is ghosting us! Start Claude Code working to access the chatroom!", "No Claude in sight! Keep Claude Code active to open the chatroom gates!", "No Claude juice detected! Keep Claude Code active to enter the chatroom!", "Claude has powered down! Start Claude Code thinking to unlock the chatroom!" ]; function ClaudeMessage() { const [index, setIndex] = React.useState(() => Math.floor(Math.random() * ClaudeMessages.length)); return /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: ClaudeMessages[index] }); } var CHAT_DEV_MODE = import.meta.url.endsWith(".tsx") && process.env.VIBECHAT_DEV === "true"; var ChatUI = ({ monitor, bannerText, announceText }) => { const [messages, setMessages] = useState([]); const [staticMessages, setStaticMessages] = useState([]); const [inputValue, setInputValue] = useState(""); const [isHidden, setIsHidden] = useState(!CHAT_DEV_MODE); const [terminalHeight, setTerminalHeight] = useState(24); const [showDisabledWarning, setShowDisabledWarning] = useState(false); const [showNetworkError, setShowNetworkError] = useState(false); const [footerMessage, setFooterMessage] = useState(""); const [activeSessions, setActiveSessions] = useState(0); const [todayCost, setTodayCost] = useState(0); const [showChatInput, setShowChatInput] = useState(false); const [_eventsChannel, setEventsChannel] = useState(null); const [username, setUsername] = useState(os2.userInfo().username); const [settings, setSettings] = useState({}); const [exitWarning, setExitWarning] = useState({ timer: null, show: false, type: "" }); const { exit } = useApp(); const { stdout } = useStdout(); const chatInputRef = useRef(null); const messagesRef = useRef(null); useEffect(() => { const loadInitialSettings = async () => { const savedSettings = await loadSettings(); setSettings(savedSettings); if (savedSettings.username) { setUsername(savedSettings.username); } }; loadInitialSettings(); }, []); useEffect(() => { let subscriptionCleanup = null; const setupEventsChannel = async () => { try { const channel = await events.connect("/default/public", auth); setEventsChannel(channel); const subscription = channel.subscribe({ next: (data) => { const messageData = data.event || data; if (messageData.type === "message" && messageData.user && messageData.text) { const newMessage = { id: messageData.id || Date.now(), user: messageData.user, amount: messageData.amount || "0x $0.00", text: messageData.text, timestamp: messageData.timestamp || (/* @__PURE__ */ new Date()).toLocaleTimeString() }; setMessages((prev) => [...prev, newMessage]); } }, error: (_err) => { } }); subscriptionCleanup = () => { subscription.unsubscribe(); }; } catch (_error) { setShowNetworkError(true); setFooterMessage("Failed to connect to chat server"); setTimeout(() => { setShowNetworkError(false); setFooterMessage(""); }, 3e3); } }; monitor.onUpdate = (stats) => { setActiveSessions(stats.activeSessions); setTodayCost(stats.todayCost); setIsHidden(CHAT_DEV_MODE ? false : stats.activeSessions === 0); }; monitor.start().catch((error) => { console.error("Failed to start monitor:", error.message); exit(); }); setupEventsChannel(); return () => { monitor.stop(); if (subscriptionCleanup) { subscriptionCleanup(); } if (exitWarning.timer) { clearTimeout(exitWarning.timer); } }; }, [monitor, exit, exitWarning.timer]); useEffect(() => { if (!isHidden) { setShowChatInput(true); } else { if (inputValue.trim() === "") { setShowChatInput(false); } } }, [isHidden, inputValue.trim]); useEffect(() => { let termHeight; if (chatInputRef.current) { try { const chatInputDims = measureElement(chatInputRef.current); termHeight = stdout.rows - chatInputDims.height - 1; } catch { termHeight = stdout.rows - 1; } } else { termHeight = stdout.rows - 1; } setTerminalHeight(termHeight); if (messagesRef.current) { try { const messageDims = measureElement(messagesRef.current); if (messageDims.height >= termHeight) { const numOverflow = Math.max(1, messages.length - termHeight); setMessages(messages.slice(numOverflow)); setStaticMessages((msgs) => [...msgs, ...messages.slice(0, numOverflow)]); } } catch { } } }); const handleExitKey = (keyType) => { if (exitWarning.timer) { if (exitWarning.type === keyType) { clearTimeout(exitWarning.timer); setExitWarning({ timer: null, show: false, type: "" }); process.kill(process.pid, "SIGTERM"); return; } else { clearTimeout(exitWarning.timer); } } const timer = setTimeout(() => { setExitWarning({ timer: null, show: false, type: "" }); }, 1500); setExitWarning({ timer, show: true, type: keyType }); }; useInput((input, key) => { if (key.escape) { handleExitKey("Escape"); return; } if (key.ctrl && input === "c") { handleExitKey("Ctrl+C"); return; } if (key.ctrl && input === "d") { handleExitKey("Ctrl+D"); } }); const handleSubmit = async () => { if (inputValue.trim()) { const trimmedInput = inputValue.trim(); if (trimmedInput.startsWith("/nick ")) { const newUsername = trimmedInput.slice(6).trim(); if (newUsername && newUsername !== username) { const oldUsername = username; setUsername(newUsername); const newSettings = { ...settings, username: newUsername }; setSettings(newSettings); saveSettings(newSettings); const nicknameChangeMessage = { type: "message", id: Date.now(), user: "System", amount: "\u{1F504}", text: `${oldUsername} changed their name to ${newUsername}`, timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString() }; try { await events.post("/default/public", nicknameChangeMessage, auth); } catch (_error) { } } setInputValue(""); return; } if (isHidden) { setShowDisabledWarning(true); setTimeout(() => setShowDisabledWarning(false), 1e3); return; } const messageData = { type: "message", id: Date.now(), user: CHAT_DEV_MODE ? `${username} \u0D9E sus \u0D9E` : username, amount: CHAT_DEV_MODE ? "$00.00 \u2620" : `$${todayCost >= 100 ? todayCost.toFixed(0) : todayCost.toFixed(2)} ${activeSessions}x`, text: trimmedInput, timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString() }; try { await events.post("/default/public", messageData, auth); setInputValue(""); } catch (_error) { setShowNetworkError(true); setFooterMessage(""); setTimeout(() => setShowNetworkError(false), 3e3); } } }; useEffect(() => { const initialMessages = []; const bannerMessage = { id: "banner", type: "banner", isBanner: true }; initialMessages.push(bannerMessage); if (announceText) { const announceMessage = { id: "announcement", user: "announcements", amount: "", text: announceText, timestamp: (/* @__PURE__ */ new Date()).toLocaleTimeString() }; initialMessages.push(announceMessage); } setMessages(initialMessages); }, [announceText]); const getUserColor = (username2) => { if (username2 === "announcements") { return "yellow"; } let hash = 0; for (let i = 0; i < username2.length; i++) { hash = username2.charCodeAt(i) + ((hash << 5) - hash); } const colors = ["red", "green", "yellow", "blue", "magenta", "cyan"]; return colors[Math.abs(hash) % colors.length]; }; const renderUsername = (username2) => { const susIndicator = " \u0D9E sus \u0D9E"; if (username2.includes(susIndicator)) { const [baseUsername, ..._rest] = username2.split(susIndicator); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Text, { bold: true, color: getUserColor(baseUsername), children: baseUsername }), /* @__PURE__ */ jsx(Text, { color: "red", children: susIndicator }) ] }); } return /* @__PURE__ */ jsx(Text, { bold: true, color: getUserColor(username2), children: username2 }); }; const renderMessage = (msg) => { if (msg.isBanner) { return /* @__PURE__ */ jsx(VibeChatLogo, { bannerText }); } const _userColor = getUserColor(msg.user); return /* @__PURE__ */ jsxs(Box, { children: [ /* @__PURE__ */ jsxs(Box, { width: 30, flexShrink: 0, justifyContent: "space-between", children: [ /* @__PURE__ */ jsx(Text, { color: "gray", children: msg.amount }), /* @__PURE__ */ jsxs(Text, { children: [ renderUsername(msg.user), /* @__PURE__ */ jsx(Text, { children: ": " }) ] }) ] }), /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: msg.text }) }) ] }); }; return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [ /* @__PURE__ */ jsx(Static, { items: staticMessages, children: (msg) => /* @__PURE__ */ jsx(Box, { children: renderMessage(msg) }, msg.id) }), isHidden ? /* @__PURE__ */ jsx( Box, { height: terminalHeight, justifyContent: "center", alignItems: "center", flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [ /* @__PURE__ */ jsx(VibeChatLogo, { bannerText }), /* @__PURE__ */ jsxs( Box, { borderStyle: "round", borderColor: "gray", width: 60, justifyContent: "center", alignItems: "center", paddingX: 3, paddingY: 1, flexDirection: "column", children: [ /* @__PURE__ */ jsx(ClaudeMessage, {}), /* @__PURE__ */ jsx(Box, { marginTop: 1, children: exitWarning.show ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [ "Press ", exitWarning.type, " again to exit" ] }) : /* @__PURE__ */ jsxs(Text, { color: "gray", children: [ "Today's spend: $", todayCost.toFixed(2) ] }) }) ] } ) ] }) } ) : /* @__PURE__ */ jsx(Box, { ref: messagesRef, flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx(Box, { children: renderMessage(msg) }, msg.id)) }), showChatInput && /* @__PURE__ */ jsxs(Box, { ref: chatInputRef, flexDirection: "column", children: [ /* @__PURE__ */ jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [ /* @__PURE__ */ jsx(Text, { bold: true, color: getUserColor(username), children: username }), CHAT_DEV_MODE && /* @__PURE__ */ jsx(Text, { color: "red", children: " (dev mode \u2014 \u0D9E sus \u0D9E) " }), /* @__PURE__ */ jsxs(Text, { color: "gray", children: [ " ", "($", todayCost >= 100 ? todayCost.toFixed(0) : todayCost.toFixed(2), " ", activeSessions, "x):", " " ] }), /* @__PURE__ */ jsx( TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit, placeholder: "Type your message..." } ) ] }), /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx( Text, { color: showNetworkError ? "red" : showDisabledWarning ? "yellow" : exitWarning.show ? "yellow" : "gray", dimColor: !showDisabledWarning && !showNetworkError && !exitWarning.show, bold: showDisabledWarning || showNetworkError || exitWarning.show, children: exitWarning.show ? `Press ${exitWarning.type} again to exit` : showNetworkError ? footerMessage || "Network error - message not sent. Press Enter to retry" : isHidden ? "Posting disabled until session resumes" : "Use /nick <name> to change username" } ) }) ] }) ] }); }; async function main() { const result = await checkVersionAndGetPricing(); const pricing = result?.pricing; const banner = result?.banner; const announce = result?.announce; const monitor = new ClaudeSessionMonitor(pricing); let isExiting = false; const shutdown = async () => { if (isExiting) return; isExiting = true; try { monitor.stop(); } catch (_error) { } process.exit(0); }; process.on("SIGTERM", shutdown); render(/* @__PURE__ */ jsx(ChatUI, { monitor, bannerText: banner, announceText: announce }), {