Files
claude-code-viewer/src/server/service/session/sessionMetaStorage.ts
2025-10-16 02:23:28 +09:00

132 lines
3.5 KiB
TypeScript

import { statSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { FileCacheStorage } from "../../lib/storage/FileCacheStorage";
import {
type ParsedCommand,
parseCommandXml,
parsedCommandSchema,
} from "../parseCommandXml";
import { parseJsonl } from "../parseJsonl";
import { sessionMetaSchema } from "../schema";
import type { SessionMeta } from "../types";
import { decodeSessionId } from "./id";
import { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
const ignoreCommands = [
"/clear",
"/login",
"/logout",
"/exit",
"/mcp",
"/memory",
];
class SessionMetaStorage {
private firstCommandCache = FileCacheStorage.load(
"first-command-cache",
parsedCommandSchema
);
private sessionMetaCache = new InMemoryCacheStorage<SessionMeta>();
public async getSessionMeta(
projectId: string,
sessionId: string
): Promise<SessionMeta> {
const cached = this.sessionMetaCache.get(sessionId);
if (cached !== undefined) {
return cached;
}
const sessionPath = decodeSessionId(projectId, sessionId);
const stats = statSync(sessionPath);
const lastModifiedUnixTime = stats.mtime.getTime();
const content = await readFile(sessionPath, "utf-8");
const lines = content.split("\n");
const sessionMeta: SessionMeta = {
messageCount: lines.length,
firstCommand: this.getFirstCommand(sessionPath, lines),
lastModifiedAt: lastModifiedUnixTime
? new Date(lastModifiedUnixTime).toISOString()
: null,
};
this.sessionMetaCache.save(sessionId, sessionMeta);
return sessionMeta;
}
private getFirstCommand = (
jsonlFilePath: string,
lines: string[]
): ParsedCommand | null => {
const cached = this.firstCommandCache.get(jsonlFilePath);
if (cached !== undefined) {
return cached;
}
let firstCommand: ParsedCommand | null = null;
for (const line of lines) {
const conversation = parseJsonl(line).at(0);
if (conversation === undefined || conversation.type !== "user") {
continue;
}
const firstUserText =
conversation === null
? null
: typeof conversation.message.content === "string"
? conversation.message.content
: (() => {
const firstContent = conversation.message.content.at(0);
if (firstContent === undefined) return null;
if (typeof firstContent === "string") return firstContent;
if (firstContent.type === "text") return firstContent.text;
return null;
})();
if (firstUserText === null) {
continue;
}
if (
firstUserText ===
"Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to."
) {
continue;
}
const command = parseCommandXml(firstUserText);
if (command.kind === "local-command") {
continue;
}
if (
command.kind === "command" &&
ignoreCommands.includes(command.commandName)
) {
continue;
}
firstCommand = command;
break;
}
if (firstCommand !== null) {
this.firstCommandCache.save(jsonlFilePath, firstCommand);
}
return firstCommand;
};
public invalidateSession(_projectId: string, sessionId: string) {
this.sessionMetaCache.invalidate(sessionId);
}
}
export const sessionMetaStorage = new SessionMetaStorage();