UNPKG

better-claude-code

Version:

CLI auxiliary tools for Claude Code

677 lines (671 loc) 23.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { CLAUDE_CODE_SESSION_COMPACTION_ID: () => CLAUDE_CODE_SESSION_COMPACTION_ID, ClaudeHelper: () => ClaudeHelper, MessageCountMode: () => MessageCountMode, MessageSource: () => MessageSource, NodeEnv: () => NodeEnv, SessionSortBy: () => SessionSortBy, TitleSource: () => TitleSource, backendEnvSchema: () => backendEnvSchema, cliEnvSchema: () => cliEnvSchema, execAsync: () => execAsync, generateUuid: () => generateUuid, getDefaultNodeEnv: () => getDefaultNodeEnv, isDistMode: () => isDistMode, listSessions: () => listSessions }); module.exports = __toCommonJS(index_exports); // src/claude-helper.ts var import_node_child_process = require("child_process"); var import_node_fs = require("fs"); var import_node_os = require("os"); var import_node_path = require("path"); var CLAUDE_CODE_SESSION_COMPACTION_ID = "CLAUDE_CODE_SESSION_COMPACTION_ID"; var MessageSource = /* @__PURE__ */ ((MessageSource2) => { MessageSource2["USER"] = "user"; MessageSource2["CC"] = "assistant"; return MessageSource2; })(MessageSource || {}); var ClaudeHelper = class _ClaudeHelper { static getClaudeDir() { return (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude"); } static getProjectsDir() { return (0, import_node_path.join)(_ClaudeHelper.getClaudeDir(), "projects"); } static getProjectDir(projectName) { return (0, import_node_path.join)(_ClaudeHelper.getProjectsDir(), projectName); } static getSessionPath(projectName, sessionId) { return (0, import_node_path.join)(_ClaudeHelper.getProjectDir(projectName), `${sessionId}.jsonl`); } static getSessionMetadataPath(projectName, sessionId) { return (0, import_node_path.join)(_ClaudeHelper.getProjectDir(projectName), ".metadata", `${sessionId}.json`); } static getSessionsPath(projectName) { return _ClaudeHelper.getProjectDir(projectName); } static getClaudeBinaryPath() { const currentPlatform = (0, import_node_os.platform)(); const homeDir = (0, import_node_os.homedir)(); switch (currentPlatform) { case "darwin": case "linux": return (0, import_node_path.join)(homeDir, ".claude", "local", "claude"); case "win32": return (0, import_node_path.join)(homeDir, ".claude", "local", "claude.exe"); default: throw new Error(`Unsupported platform: ${currentPlatform}`); } } static validateClaudeBinary() { const claudePath = _ClaudeHelper.getClaudeBinaryPath(); if (!(0, import_node_fs.existsSync)(claudePath)) { throw new Error(`Claude Code binary not found at: ${claudePath}`); } } static normalizePathForClaudeProjects(dirPath) { return dirPath.replace(/\/_/g, "--").replace(/\//g, "-").replace(/_/g, "-"); } static isUserMessage(messageSource) { return messageSource === "user" /* USER */; } static isCCMessage(messageSource) { return messageSource === "assistant" /* CC */; } static isCompactionSession(lines) { try { for (const line of lines) { try { const parsed = JSON.parse(line); if (_ClaudeHelper.isUserMessage(parsed.type)) { const messageContent = parsed.message?.content; let textContent = ""; if (typeof messageContent === "string") { textContent = messageContent; } else if (Array.isArray(messageContent)) { const textPart = messageContent.find((item) => item.type === "text"); if (textPart?.text) { textContent = textPart.text; } } if (textContent && textContent !== "Warmup" && !textContent.includes("Caveat:")) { return textContent.startsWith(CLAUDE_CODE_SESSION_COMPACTION_ID); } } } catch { } } return false; } catch { return false; } } static async executePromptNonInteractively(prompt) { _ClaudeHelper.validateClaudeBinary(); const claudePath = _ClaudeHelper.getClaudeBinaryPath(); return new Promise((resolve, reject) => { const child = (0, import_node_child_process.spawn)(claudePath, ["--dangerously-skip-permissions", "-p", prompt], { stdio: "inherit" }); child.on("exit", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Claude Code exited with code ${code}`)); } }); child.on("error", (err) => { reject(err); }); }); } }; // src/exec.ts var import_node_child_process2 = require("child_process"); var import_node_util = require("util"); var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec); // src/schemas.ts var import_zod = __toESM(require("zod"), 1); var NodeEnv = /* @__PURE__ */ ((NodeEnv2) => { NodeEnv2["DEVELOPMENT"] = "development"; NodeEnv2["PRODUCTION"] = "production"; return NodeEnv2; })(NodeEnv || {}); function isDistMode() { const stackTrace = new Error().stack || ""; return stackTrace.includes("/dist/"); } function getDefaultNodeEnv() { return isDistMode() ? "production" /* PRODUCTION */ : "development" /* DEVELOPMENT */; } var backendEnvSchema = import_zod.default.object({ SERVER_PORT: import_zod.default.coerce.number(), FRONTEND_STATIC_PATH: import_zod.default.string().optional(), //shared NODE_ENV: import_zod.default.enum(NodeEnv), SHELL: import_zod.default.string().optional() }); var cliEnvSchema = import_zod.default.object({ //shared NODE_ENV: import_zod.default.enum(NodeEnv), SHELL: import_zod.default.string().optional() }); // src/session-list.ts var import_node_fs2 = require("fs"); var import_promises = require("fs/promises"); var import_node_path2 = require("path"); var IGNORE_EMPTY_SESSIONS = true; var MAX_TITLE_LENGTH = 80; var TOKEN_LIMIT = 18e4; var CLAUDE_CODE_COMMANDS = ["clear", "ide", "model", "compact", "init"]; var MessageCountMode = /* @__PURE__ */ ((MessageCountMode2) => { MessageCountMode2["TURN"] = "turn"; MessageCountMode2["EVENT"] = "event"; return MessageCountMode2; })(MessageCountMode || {}); var TitleSource = /* @__PURE__ */ ((TitleSource2) => { TitleSource2["FIRST_USER_MESSAGE"] = "first_user_message"; TitleSource2["LAST_CC_MESSAGE"] = "last_cc_message"; return TitleSource2; })(TitleSource || {}); var SessionSortBy = /* @__PURE__ */ ((SessionSortBy2) => { SessionSortBy2["DATE"] = "date"; SessionSortBy2["TOKEN_PERCENTAGE"] = "tokenPercentage"; return SessionSortBy2; })(SessionSortBy || {}); function extractTextContent(content) { if (typeof content === "string") { return content; } if (Array.isArray(content)) { return content.filter((item) => item.type === "text").map((item) => item.text).join(" "); } return ""; } function parseCommandFromContent(content) { const commandMatch = content.match(/<command-name>\/?([^<]+)<\/command-name>/); if (commandMatch) { const commandText = commandMatch[1]; const argsMatch = content.match(/<command-args>([^<]+)<\/command-args>/); const args = argsMatch ? ` ${argsMatch[1]}` : ""; return `/${commandText}${args}`; } return null; } function isValidUserMessage(content) { if (!content || content === "Warmup") { return false; } const firstLine = content.split("\n")[0].replace(/\\/g, "").replace(/\s+/g, " ").trim(); if (firstLine.includes("Caveat:")) { return false; } const commandMatch = content.match(/<command-name>\/?([^<]+)<\/command-name>/); if (commandMatch) { const commandName = commandMatch[1]; if (CLAUDE_CODE_COMMANDS.includes(commandName)) { return false; } } const isSystemMessage = firstLine.startsWith("<local-command-") || firstLine.startsWith("[Tool:") || firstLine.startsWith("[Request interrupted"); return !isSystemMessage; } function countMessages(lines, mode) { if (mode === "turn" /* TURN */) { let userCount = 0; let assistantCount = 0; let prevType = ""; for (const line of lines) { try { const parsed = JSON.parse(line); const currentType = parsed.type; if (ClaudeHelper.isUserMessage(currentType) || ClaudeHelper.isCCMessage(currentType)) { if (ClaudeHelper.isUserMessage(currentType)) { const content = extractTextContent(parsed.message?.content); if (!isValidUserMessage(content)) { continue; } } if (currentType !== prevType) { if (ClaudeHelper.isUserMessage(currentType)) { userCount++; } else { assistantCount++; } prevType = currentType; } } } catch { } } return { userCount, assistantCount, totalCount: userCount + assistantCount }; } else { const userCount = lines.filter((line) => { try { const parsed = JSON.parse(line); if (!ClaudeHelper.isUserMessage(parsed.type)) { return false; } const content = extractTextContent(parsed.message?.content); return isValidUserMessage(content); } catch { return false; } }).length; const assistantCount = lines.filter((line) => { try { const parsed = JSON.parse(line); return ClaudeHelper.isCCMessage(parsed.type); } catch { return false; } }).length; return { userCount, assistantCount, totalCount: userCount + assistantCount }; } } function calculateTokenPercentage(lines) { for (let j = lines.length - 1; j >= 0; j--) { try { const parsed = JSON.parse(lines[j]); const usage = parsed.message?.usage; if (usage) { const inputTokens = usage.input_tokens || 0; const cacheReadTokens = usage.cache_read_input_tokens || 0; const outputTokens = usage.output_tokens || 0; const total = inputTokens + cacheReadTokens + outputTokens; if (total > 0) { return Math.floor(total * 100 / TOKEN_LIMIT); } } } catch { } } return void 0; } function extractTitle(lines, titleSource) { if (titleSource === "last_cc_message" /* LAST_CC_MESSAGE */) { for (let j = lines.length - 1; j >= 0; j--) { try { const parsed = JSON.parse(lines[j]); if (ClaudeHelper.isCCMessage(parsed.type) && Array.isArray(parsed.message?.content)) { for (const item of parsed.message.content) { if (item.type === "text" && item.text) { let title = item.text.replace(/\n/g, " ").trim(); if (title.length > MAX_TITLE_LENGTH) { title = `${title.substring(0, MAX_TITLE_LENGTH)}...`; } return title; } } } } catch { } } return "Empty session"; } for (const line of lines) { try { const parsed = JSON.parse(line); if (ClaudeHelper.isUserMessage(parsed.type)) { const content = extractTextContent(parsed.message?.content); if (content && content !== "Warmup") { const firstLine = content.split("\n")[0].replace(/\\/g, "").replace(/\s+/g, " ").trim(); if (firstLine.includes("Caveat:")) { continue; } const parsedCommand = parseCommandFromContent(content); if (parsedCommand) { const commandMatch = content.match(/<command-name>\/?([^<]+)<\/command-name>/); const commandName = commandMatch?.[1]; if (commandName && CLAUDE_CODE_COMMANDS.includes(commandName)) { continue; } } const isSystemMessage = firstLine.startsWith("<local-command-") || firstLine.startsWith("[Tool:") || firstLine.startsWith("[Request interrupted"); if (isSystemMessage) { continue; } let title = parsedCommand || content.replace(/\\/g, "").replace(/\n/g, " ").replace(/\s+/g, " ").trim(); if (title.length > MAX_TITLE_LENGTH) { title = `${title.substring(0, MAX_TITLE_LENGTH)}...`; } return title; } } } catch { } } return ""; } function countImages(lines) { let count = 0; for (const line of lines) { try { const parsed = JSON.parse(line); const messageContent = parsed.message?.content; if (Array.isArray(messageContent)) { for (const item of messageContent) { if (item.type === "image") { count++; } } } } catch { } } return count; } function countCustomCommands(lines) { let count = 0; for (const line of lines) { try { const parsed = JSON.parse(line); if (ClaudeHelper.isUserMessage(parsed.type)) { const content = extractTextContent(parsed.message?.content); if (content) { const commandMatch = content.match(/<command-name>\/?([^<]+)<\/command-name>/); if (commandMatch) { const commandName = commandMatch[1]; if (!CLAUDE_CODE_COMMANDS.includes(commandName)) { count++; } } } } } catch { } } return count; } function countFilesOrFolders(lines) { let count = 0; for (const line of lines) { try { const parsed = JSON.parse(line); if (ClaudeHelper.isUserMessage(parsed.type)) { const content = extractTextContent(parsed.message?.content); if (content) { const fileOrFolderMatches = content.match(/@[\w\-./]+/g); if (fileOrFolderMatches) { count += fileOrFolderMatches.length; } } } } catch { } } return count; } function countUrls(lines) { let count = 0; const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\]]+/g; for (const line of lines) { try { const parsed = JSON.parse(line); if (ClaudeHelper.isUserMessage(parsed.type)) { const content = extractTextContent(parsed.message?.content); if (content) { const urlMatches = content.match(urlRegex); if (urlMatches) { count += urlMatches.length; } } } } catch { } } return count; } async function readLabels(sessionsPath, sessionId) { const metadataPath = (0, import_node_path2.join)(sessionsPath, ".metadata", `${sessionId}.json`); try { const metadataContent = await (0, import_promises.readFile)(metadataPath, "utf-8"); const metadata = JSON.parse(metadataContent); return metadata.labels?.length > 0 ? metadata.labels : void 0; } catch { return void 0; } } async function processSessionFile(filePath, file, sessionsPath, options) { const content = await (0, import_promises.readFile)(filePath, "utf-8"); const lines = content.trim().split("\n").filter(Boolean); if (ClaudeHelper.isCompactionSession(lines)) return null; const firstUserMessage = extractTextContent( lines.map((line) => { try { const parsed = JSON.parse(line); if (ClaudeHelper.isUserMessage(parsed.type)) { return parsed.message?.content; } } catch { } return null; }).find((msg) => msg) ); if (firstUserMessage.startsWith(CLAUDE_CODE_SESSION_COMPACTION_ID)) { return null; } const title = extractTitle(lines, options.titleSource); if (!title && IGNORE_EMPTY_SESSIONS) { return null; } let searchMatchCount; if (options.search) { const searchLower = options.search.toLowerCase(); const titleMatch = title.toLowerCase().includes(searchLower); let contentMatches = 0; for (const line of lines) { try { const parsed = JSON.parse(line); if (ClaudeHelper.isUserMessage(parsed.type) || ClaudeHelper.isCCMessage(parsed.type)) { const messageContent = extractTextContent(parsed.message?.content); if (messageContent) { const matches = messageContent.toLowerCase().split(searchLower).length - 1; contentMatches += matches; } } } catch { } } if (!titleMatch && contentMatches === 0) { return null; } searchMatchCount = contentMatches + (titleMatch ? 1 : 0); } const tokenPercentage = calculateTokenPercentage(lines); const messageCounts = countMessages(lines, options.messageCountMode); const stats = await (0, import_promises.stat)(filePath); const sessionId = file.replace(".jsonl", ""); const session = { id: sessionId, title: title || "Empty session", messageCount: messageCounts.totalCount, createdAt: stats.birthtimeMs, tokenPercentage, searchMatchCount, filePath, shortId: sessionId.slice(-12), userMessageCount: messageCounts.userCount, assistantMessageCount: messageCounts.assistantCount }; if (options.includeImages) { const imageCount = countImages(lines); if (imageCount > 0) session.imageCount = imageCount; } if (options.includeCustomCommands) { const customCommandCount = countCustomCommands(lines); if (customCommandCount > 0) session.customCommandCount = customCommandCount; } if (options.includeFilesOrFolders) { const filesOrFoldersCount = countFilesOrFolders(lines); if (filesOrFoldersCount > 0) session.filesOrFoldersCount = filesOrFoldersCount; } if (options.includeUrls) { const urlCount = countUrls(lines); if (urlCount > 0) session.urlCount = urlCount; } if (options.includeLabels) { const labels = await readLabels(sessionsPath, sessionId); if (labels) session.labels = labels; } return session; } async function listSessions(options) { const { projectPath, limit = 20, page = 1, search = "", sortBy = "date" /* DATE */, messageCountMode = "event" /* EVENT */, titleSource = "first_user_message" /* FIRST_USER_MESSAGE */, includeImages = false, includeCustomCommands = false, includeFilesOrFolders = false, includeUrls = false, includeLabels = false, enablePagination = false } = options; const normalizedPath = ClaudeHelper.normalizePathForClaudeProjects(projectPath); const sessionsPath = ClaudeHelper.getProjectDir(normalizedPath); if (!(0, import_node_fs2.existsSync)(sessionsPath)) { throw new Error(`Project directory not found: ${sessionsPath}`); } const files = await (0, import_promises.readdir)(sessionsPath); const sessionFiles = files.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-")); const processOptions = { search, messageCountMode, titleSource, includeImages, includeCustomCommands, includeFilesOrFolders, includeUrls, includeLabels }; let sessions; if (!search && sortBy === "date" /* DATE */) { const fileStatsPromises = sessionFiles.map(async (file) => { const filePath = (0, import_node_path2.join)(sessionsPath, file); const stats = await (0, import_promises.stat)(filePath); return { file, filePath, birthtimeMs: stats.birthtimeMs }; }); const fileStats = await Promise.all(fileStatsPromises); fileStats.sort((a, b) => b.birthtimeMs - a.birthtimeMs); if (enablePagination) { const sessionPromises3 = fileStats.map( ({ file, filePath }) => processSessionFile(filePath, file, sessionsPath, processOptions) ); const sessionsResults3 = await Promise.all(sessionPromises3); const allValidSessions = sessionsResults3.filter((s) => s !== null); const totalItems = allValidSessions.length; const totalPages = Math.ceil(totalItems / limit); const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedSessions = allValidSessions.slice(startIndex, endIndex); return { items: paginatedSessions, meta: { totalItems, totalPages, page, limit } }; } const filesToProcess = fileStats.slice(0, limit); const sessionPromises2 = filesToProcess.map( ({ file, filePath }) => processSessionFile(filePath, file, sessionsPath, processOptions) ); const sessionsResults2 = await Promise.all(sessionPromises2); sessions = sessionsResults2.filter((s) => s !== null); return { items: sessions }; } const sessionPromises = sessionFiles.map((file) => { const filePath = (0, import_node_path2.join)(sessionsPath, file); return processSessionFile(filePath, file, sessionsPath, processOptions); }); const sessionsResults = await Promise.all(sessionPromises); sessions = sessionsResults.filter((s) => s !== null); if (sortBy === "tokenPercentage" /* TOKEN_PERCENTAGE */) { sessions.sort((a, b) => { const aToken = a.tokenPercentage ?? -1; const bToken = b.tokenPercentage ?? -1; return bToken - aToken; }); } else { sessions.sort((a, b) => b.createdAt - a.createdAt); } if (enablePagination) { const totalItems = sessions.length; const totalPages = Math.ceil(totalItems / limit); const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedSessions = sessions.slice(startIndex, endIndex); return { items: paginatedSessions, meta: { totalItems, totalPages, page, limit } }; } const limitedSessions = sessions.slice(0, limit); return { items: limitedSessions }; } // src/uuid.ts var import_node_crypto = require("crypto"); function generateUuid() { return (0, import_node_crypto.randomUUID)(); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CLAUDE_CODE_SESSION_COMPACTION_ID, ClaudeHelper, MessageCountMode, MessageSource, NodeEnv, SessionSortBy, TitleSource, backendEnvSchema, cliEnvSchema, execAsync, generateUuid, getDefaultNodeEnv, isDistMode, listSessions }); //# sourceMappingURL=index.cjs.map