better-claude-code
Version:
CLI auxiliary tools for Claude Code
677 lines (671 loc) • 23.3 kB
JavaScript
;
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