mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-21 23:34:20 +01:00
perf: added cache for project, session
This commit is contained in:
@@ -2,7 +2,7 @@ import { handle } from "hono/vercel";
|
||||
import { honoApp } from "../../../server/hono/app";
|
||||
import { routes } from "../../../server/hono/route";
|
||||
|
||||
routes(honoApp);
|
||||
await routes(honoApp);
|
||||
|
||||
export const GET = handle(honoApp);
|
||||
export const POST = handle(honoApp);
|
||||
|
||||
41
src/server/hono/initialize.ts
Normal file
41
src/server/hono/initialize.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { eventBus } from "../service/events/EventBus";
|
||||
import { fileWatcher } from "../service/events/fileWatcher";
|
||||
import type { ProjectRepository } from "../service/project/ProjectRepository";
|
||||
import { projectMetaStorage } from "../service/project/projectMetaStorage";
|
||||
import type { SessionRepository } from "../service/session/SessionRepository";
|
||||
import { sessionMetaStorage } from "../service/session/sessionMetaStorage";
|
||||
|
||||
export const initialize = async (deps: {
|
||||
sessionRepository: SessionRepository;
|
||||
projectRepository: ProjectRepository;
|
||||
}): Promise<void> => {
|
||||
fileWatcher.startWatching();
|
||||
|
||||
setInterval(() => {
|
||||
eventBus.emit("heartbeat", {});
|
||||
}, 10 * 1000);
|
||||
|
||||
eventBus.on("sessionChanged", (event) => {
|
||||
projectMetaStorage.invalidateProject(event.projectId);
|
||||
sessionMetaStorage.invalidateSession(event.projectId, event.sessionId);
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("Initializing projects cache");
|
||||
const { projects } = await deps.projectRepository.getProjects();
|
||||
console.log(`${projects.length} projects cache initialized`);
|
||||
|
||||
console.log("Initializing sessions cache");
|
||||
const results = await Promise.all(
|
||||
projects.map((project) => deps.sessionRepository.getSessions(project.id))
|
||||
);
|
||||
console.log(
|
||||
`${results.reduce(
|
||||
(s, { sessions }) => s + sessions.length,
|
||||
0
|
||||
)} sessions cache initialized`
|
||||
);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
@@ -9,8 +9,7 @@ import { env } from "../lib/env";
|
||||
import { ClaudeCodeTaskController } from "../service/claude-code/ClaudeCodeTaskController";
|
||||
import type { SerializableAliveTask } from "../service/claude-code/types";
|
||||
import { adaptInternalEventToSSE } from "../service/events/adaptInternalEventToSSE";
|
||||
import { getEventBus } from "../service/events/EventBus";
|
||||
import { getFileWatcher } from "../service/events/fileWatcher";
|
||||
import { eventBus } from "../service/events/EventBus";
|
||||
import type { InternalEventDeclaration } from "../service/events/InternalEventDeclaration";
|
||||
import { writeTypeSafeSSE } from "../service/events/typeSafeSSE";
|
||||
import { getFileCompletion } from "../service/file-completion/getFileCompletion";
|
||||
@@ -19,14 +18,13 @@ import { getCommits } from "../service/git/getCommits";
|
||||
import { getDiff } from "../service/git/getDiff";
|
||||
import { getMcpList } from "../service/mcp/getMcpList";
|
||||
import { claudeCommandsDirPath } from "../service/paths";
|
||||
import { getProject } from "../service/project/getProject";
|
||||
import { getProjects } from "../service/project/getProjects";
|
||||
import { getSession } from "../service/session/getSession";
|
||||
import { getSessions } from "../service/session/getSessions";
|
||||
import { ProjectRepository } from "../service/project/ProjectRepository";
|
||||
import { SessionRepository } from "../service/session/SessionRepository";
|
||||
import type { HonoAppType } from "./app";
|
||||
import { initialize } from "./initialize";
|
||||
import { configMiddleware } from "./middleware/config.middleware";
|
||||
|
||||
export const routes = (app: HonoAppType) => {
|
||||
export const routes = async (app: HonoAppType) => {
|
||||
let taskController: ClaudeCodeTaskController | null = null;
|
||||
const getTaskController = (config: Config) => {
|
||||
if (!taskController) {
|
||||
@@ -37,15 +35,14 @@ export const routes = (app: HonoAppType) => {
|
||||
return taskController;
|
||||
};
|
||||
|
||||
const fileWatcher = getFileWatcher();
|
||||
const eventBus = getEventBus();
|
||||
const sessionRepository = new SessionRepository();
|
||||
const projectRepository = new ProjectRepository();
|
||||
|
||||
if (env.get("NEXT_PHASE") !== "phase-production-build") {
|
||||
fileWatcher.startWatching();
|
||||
|
||||
setInterval(() => {
|
||||
eventBus.emit("heartbeat", {});
|
||||
}, 10 * 1000);
|
||||
await initialize({
|
||||
sessionRepository,
|
||||
projectRepository,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -71,7 +68,7 @@ export const routes = (app: HonoAppType) => {
|
||||
})
|
||||
|
||||
.get("/projects", async (c) => {
|
||||
const { projects } = await getProjects();
|
||||
const { projects } = await projectRepository.getProjects();
|
||||
return c.json({ projects });
|
||||
})
|
||||
|
||||
@@ -79,8 +76,8 @@ export const routes = (app: HonoAppType) => {
|
||||
const { projectId } = c.req.param();
|
||||
|
||||
const [{ project }, { sessions }] = await Promise.all([
|
||||
getProject(projectId),
|
||||
getSessions(projectId).then(({ sessions }) => {
|
||||
projectRepository.getProject(projectId),
|
||||
sessionRepository.getSessions(projectId).then(({ sessions }) => {
|
||||
let filteredSessions = sessions;
|
||||
|
||||
// Filter sessions based on hideNoUserMessageSession setting
|
||||
@@ -157,7 +154,10 @@ export const routes = (app: HonoAppType) => {
|
||||
|
||||
.get("/projects/:projectId/sessions/:sessionId", async (c) => {
|
||||
const { projectId, sessionId } = c.req.param();
|
||||
const { session } = await getSession(projectId, sessionId);
|
||||
const { session } = await sessionRepository.getSession(
|
||||
projectId,
|
||||
sessionId,
|
||||
);
|
||||
return c.json({ session });
|
||||
})
|
||||
|
||||
@@ -173,7 +173,7 @@ export const routes = (app: HonoAppType) => {
|
||||
const { projectId } = c.req.param();
|
||||
const { basePath } = c.req.valid("query");
|
||||
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
if (project.meta.projectPath === null) {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
@@ -194,7 +194,7 @@ export const routes = (app: HonoAppType) => {
|
||||
|
||||
.get("/projects/:projectId/claude-commands", async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
const [globalCommands, projectCommands] = await Promise.allSettled([
|
||||
readdir(claudeCommandsDirPath, {
|
||||
@@ -229,7 +229,7 @@ export const routes = (app: HonoAppType) => {
|
||||
|
||||
.get("/projects/:projectId/git/branches", async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
if (project.meta.projectPath === null) {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
@@ -249,7 +249,7 @@ export const routes = (app: HonoAppType) => {
|
||||
|
||||
.get("/projects/:projectId/git/commits", async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
if (project.meta.projectPath === null) {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
@@ -279,7 +279,7 @@ export const routes = (app: HonoAppType) => {
|
||||
async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
const { fromRef, toRef } = c.req.valid("json");
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
if (project.meta.projectPath === null) {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
@@ -318,7 +318,7 @@ export const routes = (app: HonoAppType) => {
|
||||
async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
const { message } = c.req.valid("json");
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
if (project.meta.projectPath === null) {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
@@ -353,7 +353,7 @@ export const routes = (app: HonoAppType) => {
|
||||
async (c) => {
|
||||
const { projectId, sessionId } = c.req.param();
|
||||
const { resumeMessage } = c.req.valid("json");
|
||||
const { project } = await getProject(projectId);
|
||||
const { project } = await projectRepository.getProject(projectId);
|
||||
|
||||
if (project.meta.projectPath === null) {
|
||||
return c.json({ error: "Project path not found" }, 400);
|
||||
@@ -473,4 +473,4 @@ export const routes = (app: HonoAppType) => {
|
||||
);
|
||||
};
|
||||
|
||||
export type RouteType = ReturnType<typeof routes>;
|
||||
export type RouteType = Awaited<ReturnType<typeof routes>>;
|
||||
|
||||
82
src/server/lib/storage/FileCacheStorage.ts
Normal file
82
src/server/lib/storage/FileCacheStorage.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { claudeCodeViewerCacheDirPath } from "../../service/paths";
|
||||
|
||||
const saveSchema = z.array(z.tuple([z.string(), z.unknown()]));
|
||||
|
||||
export class FileCacheStorage<const T> {
|
||||
private storage = new Map<string, T>();
|
||||
|
||||
private constructor(private readonly key: string) {}
|
||||
|
||||
public static load<const LoadSchema>(
|
||||
key: string,
|
||||
schema: z.ZodType<LoadSchema>
|
||||
) {
|
||||
const instance = new FileCacheStorage<LoadSchema>(key);
|
||||
|
||||
if (!existsSync(claudeCodeViewerCacheDirPath)) {
|
||||
mkdirSync(claudeCodeViewerCacheDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
if (!existsSync(instance.cacheFilePath)) {
|
||||
writeFileSync(instance.cacheFilePath, "[]");
|
||||
} else {
|
||||
const content = readFileSync(instance.cacheFilePath, "utf-8");
|
||||
const parsed = saveSchema.safeParse(JSON.parse(content));
|
||||
|
||||
if (!parsed.success) {
|
||||
writeFileSync(instance.cacheFilePath, "[]");
|
||||
} else {
|
||||
for (const [key, value] of parsed.data) {
|
||||
const parsedValue = schema.safeParse(value);
|
||||
if (!parsedValue.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
instance.storage.set(key, parsedValue.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private get cacheFilePath() {
|
||||
return resolve(claudeCodeViewerCacheDirPath, `${this.key}.json`);
|
||||
}
|
||||
|
||||
private asSaveFormat() {
|
||||
return JSON.stringify(Array.from(this.storage.entries()));
|
||||
}
|
||||
|
||||
private async syncToFile() {
|
||||
await writeFile(this.cacheFilePath, this.asSaveFormat());
|
||||
}
|
||||
|
||||
public get(key: string) {
|
||||
return this.storage.get(key);
|
||||
}
|
||||
|
||||
public save(key: string, value: T) {
|
||||
const previous = this.asSaveFormat();
|
||||
this.storage.set(key, value);
|
||||
|
||||
if (previous === this.asSaveFormat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.syncToFile();
|
||||
}
|
||||
|
||||
public invalidate(key: string) {
|
||||
if (!this.storage.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.delete(key);
|
||||
void this.syncToFile();
|
||||
}
|
||||
}
|
||||
21
src/server/lib/storage/InMemoryCacheStorage.ts
Normal file
21
src/server/lib/storage/InMemoryCacheStorage.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export class InMemoryCacheStorage<const T> {
|
||||
private storage = new Map<string, T>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public get(key: string) {
|
||||
return this.storage.get(key);
|
||||
}
|
||||
|
||||
public save(key: string, value: T) {
|
||||
this.storage.set(key, value);
|
||||
}
|
||||
|
||||
public invalidate(key: string) {
|
||||
if (!this.storage.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import prexit from "prexit";
|
||||
import { ulid } from "ulid";
|
||||
import type { Config } from "../../config/config";
|
||||
import { getEventBus, type IEventBus } from "../events/EventBus";
|
||||
import { eventBus } from "../events/EventBus";
|
||||
import { ClaudeCodeExecutor } from "./ClaudeCodeExecutor";
|
||||
import { createMessageGenerator } from "./createMessageGenerator";
|
||||
import type {
|
||||
@@ -16,14 +16,12 @@ import type {
|
||||
export class ClaudeCodeTaskController {
|
||||
private claudeCode: ClaudeCodeExecutor;
|
||||
private tasks: ClaudeCodeTask[] = [];
|
||||
private eventBus: IEventBus;
|
||||
private config: Config;
|
||||
private pendingPermissionRequests: Map<string, PermissionRequest> = new Map();
|
||||
private permissionResponses: Map<string, PermissionResponse> = new Map();
|
||||
|
||||
constructor(config: Config) {
|
||||
this.claudeCode = new ClaudeCodeExecutor();
|
||||
this.eventBus = getEventBus();
|
||||
this.config = config;
|
||||
|
||||
prexit(() => {
|
||||
@@ -85,7 +83,7 @@ export class ClaudeCodeTaskController {
|
||||
);
|
||||
|
||||
// Emit event to notify UI
|
||||
this.eventBus.emit("permissionRequested", {
|
||||
eventBus.emit("permissionRequested", {
|
||||
permissionRequest,
|
||||
});
|
||||
|
||||
@@ -389,7 +387,7 @@ export class ClaudeCodeTaskController {
|
||||
}
|
||||
|
||||
if (task.status === "paused" || task.status === "running") {
|
||||
this.eventBus.emit("taskChanged", {
|
||||
eventBus.emit("taskChanged", {
|
||||
aliveTasks: this.aliveTasks,
|
||||
changed: task,
|
||||
});
|
||||
|
||||
@@ -36,12 +36,4 @@ class EventBus {
|
||||
}
|
||||
}
|
||||
|
||||
// singleton
|
||||
let eventBus: EventBus | null = null;
|
||||
|
||||
export const getEventBus = () => {
|
||||
eventBus ??= new EventBus();
|
||||
return eventBus;
|
||||
};
|
||||
|
||||
export type IEventBus = ReturnType<typeof getEventBus>;
|
||||
export const eventBus = new EventBus();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SSEStreamingApi } from "hono/streaming";
|
||||
import { getEventBus } from "./EventBus";
|
||||
import { eventBus } from "./EventBus";
|
||||
import type { InternalEventDeclaration } from "./InternalEventDeclaration";
|
||||
import { writeTypeSafeSSE } from "./typeSafeSSE";
|
||||
|
||||
@@ -14,8 +14,6 @@ export const adaptInternalEventToSSE = (
|
||||
|
||||
console.log("SSE connection started");
|
||||
|
||||
const eventBus = getEventBus();
|
||||
|
||||
const stream = writeTypeSafeSSE(rawStream);
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FSWatcher, watch } from "node:fs";
|
||||
import z from "zod";
|
||||
import { claudeProjectsDirPath } from "../paths";
|
||||
import { getEventBus, type IEventBus } from "./EventBus";
|
||||
import { eventBus } from "./EventBus";
|
||||
|
||||
const fileRegExp = /(?<projectId>.*?)\/(?<sessionId>.*?)\.jsonl/;
|
||||
const fileRegExpGroupSchema = z.object({
|
||||
@@ -13,11 +13,6 @@ export class FileWatcherService {
|
||||
private isWatching = false;
|
||||
private watcher: FSWatcher | null = null;
|
||||
private projectWatchers: Map<string, FSWatcher> = new Map();
|
||||
private eventBus: IEventBus;
|
||||
|
||||
constructor() {
|
||||
this.eventBus = getEventBus();
|
||||
}
|
||||
|
||||
public startWatching(): void {
|
||||
if (this.isWatching) return;
|
||||
@@ -42,13 +37,13 @@ export class FileWatcherService {
|
||||
|
||||
if (eventType === "change") {
|
||||
// セッションファイルの中身が変更されている
|
||||
this.eventBus.emit("sessionChanged", {
|
||||
eventBus.emit("sessionChanged", {
|
||||
projectId,
|
||||
sessionId,
|
||||
});
|
||||
} else if (eventType === "rename") {
|
||||
// セッションファイルの追加/削除
|
||||
this.eventBus.emit("sessionListChanged", {
|
||||
eventBus.emit("sessionListChanged", {
|
||||
projectId,
|
||||
});
|
||||
} else {
|
||||
@@ -75,13 +70,4 @@ export class FileWatcherService {
|
||||
}
|
||||
}
|
||||
|
||||
// シングルトンインスタンス
|
||||
let watcherInstance: FileWatcherService | null = null;
|
||||
|
||||
export const getFileWatcher = (): FileWatcherService => {
|
||||
if (!watcherInstance) {
|
||||
console.log("Creating new FileWatcher instance");
|
||||
watcherInstance = new FileWatcherService();
|
||||
}
|
||||
return watcherInstance;
|
||||
};
|
||||
export const fileWatcher = new FileWatcherService();
|
||||
|
||||
@@ -7,21 +7,24 @@ const matchSchema = z.object({
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
export type ParsedCommand =
|
||||
| {
|
||||
kind: "command";
|
||||
commandName: string;
|
||||
commandArgs?: string;
|
||||
commandMessage?: string;
|
||||
}
|
||||
| {
|
||||
kind: "local-command";
|
||||
stdout: string;
|
||||
}
|
||||
| {
|
||||
kind: "text";
|
||||
content: string;
|
||||
};
|
||||
export const parsedCommandSchema = z.union([
|
||||
z.object({
|
||||
kind: z.literal("command"),
|
||||
commandName: z.string(),
|
||||
commandArgs: z.string().optional(),
|
||||
commandMessage: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("local-command"),
|
||||
stdout: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("text"),
|
||||
content: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type ParsedCommand = z.infer<typeof parsedCommandSchema>;
|
||||
|
||||
export const parseCommandXml = (content: string): ParsedCommand => {
|
||||
const matches = Array.from(content.matchAll(regExp))
|
||||
|
||||
@@ -18,3 +18,9 @@ export const claudeCommandsDirPath = resolve(
|
||||
globalClaudeDirectoryPath,
|
||||
"commands",
|
||||
);
|
||||
|
||||
export const claudeCodeViewerCacheDirPath = resolve(
|
||||
homedir(),
|
||||
".claude-code-viewer",
|
||||
"cache",
|
||||
);
|
||||
|
||||
75
src/server/service/project/ProjectRepository.ts
Normal file
75
src/server/service/project/ProjectRepository.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { access, constants, readdir } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { claudeProjectsDirPath } from "../paths";
|
||||
import type { Project } from "../types";
|
||||
import { decodeProjectId, encodeProjectId } from "./id";
|
||||
import { projectMetaStorage } from "./projectMetaStorage";
|
||||
|
||||
export class ProjectRepository {
|
||||
public async getProject(projectId: string): Promise<{ project: Project }> {
|
||||
const fullPath = decodeProjectId(projectId);
|
||||
if (!existsSync(fullPath)) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const meta = await projectMetaStorage.getProjectMeta(projectId);
|
||||
|
||||
return {
|
||||
project: {
|
||||
id: projectId,
|
||||
claudeProjectPath: fullPath,
|
||||
meta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async getProjects(): Promise<{ projects: Project[] }> {
|
||||
try {
|
||||
// Check if the claude projects directory exists
|
||||
await access(claudeProjectsDirPath, constants.F_OK);
|
||||
} catch (_error) {
|
||||
// Directory doesn't exist, return empty array
|
||||
console.warn(
|
||||
`Claude projects directory not found at ${claudeProjectsDirPath}`,
|
||||
);
|
||||
return { projects: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const dirents = await readdir(claudeProjectsDirPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const projects = await Promise.all(
|
||||
dirents
|
||||
.filter((d) => d.isDirectory())
|
||||
.map(async (d) => {
|
||||
const fullPath = resolve(claudeProjectsDirPath, d.name);
|
||||
const id = encodeProjectId(fullPath);
|
||||
|
||||
return {
|
||||
id,
|
||||
claudeProjectPath: fullPath,
|
||||
meta: await projectMetaStorage.getProjectMeta(id),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
projects: projects.sort((a, b) => {
|
||||
return (
|
||||
(b.meta.lastModifiedAt
|
||||
? new Date(b.meta.lastModifiedAt).getTime()
|
||||
: 0) -
|
||||
(a.meta.lastModifiedAt
|
||||
? new Date(a.meta.lastModifiedAt).getTime()
|
||||
: 0)
|
||||
);
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error reading projects:", error);
|
||||
return { projects: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
import type { Project } from "../types";
|
||||
import { getProjectMeta } from "./getProjectMeta";
|
||||
import { decodeProjectId } from "./id";
|
||||
|
||||
export const getProject = async (
|
||||
projectId: string,
|
||||
): Promise<{ project: Project }> => {
|
||||
const fullPath = decodeProjectId(projectId);
|
||||
if (!existsSync(fullPath)) {
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const meta = await getProjectMeta(fullPath);
|
||||
|
||||
return {
|
||||
project: {
|
||||
id: projectId,
|
||||
claudeProjectPath: fullPath,
|
||||
meta,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { basename, resolve } from "node:path";
|
||||
|
||||
import { parseJsonl } from "../parseJsonl";
|
||||
import type { ProjectMeta } from "../types";
|
||||
|
||||
const projectPathCache = new Map<string, string | null>();
|
||||
|
||||
const extractProjectPathFromJsonl = async (filePath: string) => {
|
||||
const cached = projectPathCache.get(filePath);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
let cwd: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const conversation = parseJsonl(line).at(0);
|
||||
|
||||
if (
|
||||
conversation === undefined ||
|
||||
conversation.type === "summary" ||
|
||||
conversation.type === "x-error"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cwd = conversation.cwd;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (cwd !== null) {
|
||||
projectPathCache.set(filePath, cwd);
|
||||
}
|
||||
|
||||
return cwd;
|
||||
};
|
||||
|
||||
export const getProjectMeta = async (
|
||||
claudeProjectPath: string,
|
||||
): Promise<ProjectMeta> => {
|
||||
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
|
||||
const files = dirents
|
||||
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
|
||||
.map(
|
||||
(d) =>
|
||||
({
|
||||
fullPath: resolve(claudeProjectPath, d.name),
|
||||
stats: statSync(resolve(claudeProjectPath, d.name)),
|
||||
}) as const,
|
||||
)
|
||||
.sort((a, b) => {
|
||||
return a.stats.ctime.getTime() - b.stats.ctime.getTime();
|
||||
});
|
||||
|
||||
const lastModifiedUnixTime = files.at(-1)?.stats.ctime.getTime();
|
||||
|
||||
let projectPath: string | null = null;
|
||||
|
||||
for (const file of files) {
|
||||
projectPath = await extractProjectPathFromJsonl(file.fullPath);
|
||||
|
||||
if (projectPath === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const projectMeta: ProjectMeta = {
|
||||
projectName: projectPath ? basename(projectPath) : null,
|
||||
projectPath,
|
||||
lastModifiedAt: lastModifiedUnixTime
|
||||
? new Date(lastModifiedUnixTime)
|
||||
: null,
|
||||
sessionCount: files.length,
|
||||
};
|
||||
|
||||
return projectMeta;
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import { constants } from "node:fs";
|
||||
import { access, readdir } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { claudeProjectsDirPath } from "../paths";
|
||||
import type { Project } from "../types";
|
||||
import { getProjectMeta } from "./getProjectMeta";
|
||||
import { encodeProjectId } from "./id";
|
||||
|
||||
export const getProjects = async (): Promise<{ projects: Project[] }> => {
|
||||
try {
|
||||
// Check if the claude projects directory exists
|
||||
await access(claudeProjectsDirPath, constants.F_OK);
|
||||
} catch (_error) {
|
||||
// Directory doesn't exist, return empty array
|
||||
console.warn(
|
||||
`Claude projects directory not found at ${claudeProjectsDirPath}`,
|
||||
);
|
||||
return { projects: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const dirents = await readdir(claudeProjectsDirPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const projects = await Promise.all(
|
||||
dirents
|
||||
.filter((d) => d.isDirectory())
|
||||
.map(async (d) => {
|
||||
const fullPath = resolve(claudeProjectsDirPath, d.name);
|
||||
const id = encodeProjectId(fullPath);
|
||||
|
||||
return {
|
||||
id,
|
||||
claudeProjectPath: fullPath,
|
||||
meta: await getProjectMeta(fullPath),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
projects: projects.sort((a, b) => {
|
||||
return (
|
||||
(b.meta.lastModifiedAt?.getTime() ?? 0) -
|
||||
(a.meta.lastModifiedAt?.getTime() ?? 0)
|
||||
);
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error reading projects:", error);
|
||||
return { projects: [] };
|
||||
}
|
||||
};
|
||||
109
src/server/service/project/projectMetaStorage.ts
Normal file
109
src/server/service/project/projectMetaStorage.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { basename, resolve } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { FileCacheStorage } from "../../lib/storage/FileCacheStorage";
|
||||
import { parseJsonl } from "../parseJsonl";
|
||||
import type { ProjectMeta } from "../types";
|
||||
import { decodeProjectId } from "./id";
|
||||
import { InMemoryCacheStorage } from "../../lib/storage/InMemoryCacheStorage";
|
||||
|
||||
class ProjectMetaStorage {
|
||||
private projectPathCache = FileCacheStorage.load(
|
||||
"project-path-cache",
|
||||
z.string().nullable()
|
||||
);
|
||||
private projectMetaCache = new InMemoryCacheStorage<ProjectMeta>();
|
||||
|
||||
public async getProjectMeta(projectId: string): Promise<ProjectMeta> {
|
||||
const cached = this.projectMetaCache.get(projectId);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const claudeProjectPath = decodeProjectId(projectId);
|
||||
|
||||
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
|
||||
const files = dirents
|
||||
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
|
||||
.map(
|
||||
(d) =>
|
||||
({
|
||||
fullPath: resolve(claudeProjectPath, d.name),
|
||||
stats: statSync(resolve(claudeProjectPath, d.name)),
|
||||
} as const)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
return a.stats.mtime.getTime() - b.stats.mtime.getTime();
|
||||
});
|
||||
|
||||
const lastModifiedUnixTime = files.at(-1)?.stats.mtime.getTime();
|
||||
|
||||
let projectPath: string | null = null;
|
||||
|
||||
for (const file of files) {
|
||||
projectPath = await this.extractProjectPathFromJsonl(file.fullPath);
|
||||
|
||||
if (projectPath === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const projectMeta: ProjectMeta = {
|
||||
projectName: projectPath ? basename(projectPath) : null,
|
||||
projectPath,
|
||||
lastModifiedAt: lastModifiedUnixTime
|
||||
? new Date(lastModifiedUnixTime).toISOString()
|
||||
: null,
|
||||
sessionCount: files.length,
|
||||
};
|
||||
|
||||
this.projectMetaCache.save(projectId, projectMeta);
|
||||
|
||||
return projectMeta;
|
||||
}
|
||||
|
||||
public invalidateProject(projectId: string) {
|
||||
this.projectMetaCache.invalidate(projectId);
|
||||
}
|
||||
|
||||
private async extractProjectPathFromJsonl(
|
||||
filePath: string
|
||||
): Promise<string | null> {
|
||||
const cached = this.projectPathCache.get(filePath);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
let cwd: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const conversation = parseJsonl(line).at(0);
|
||||
|
||||
if (
|
||||
conversation === undefined ||
|
||||
conversation.type === "summary" ||
|
||||
conversation.type === "x-error"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cwd = conversation.cwd;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (cwd !== null) {
|
||||
this.projectPathCache.save(filePath, cwd);
|
||||
}
|
||||
|
||||
return cwd;
|
||||
}
|
||||
}
|
||||
|
||||
export const projectMetaStorage = new ProjectMetaStorage();
|
||||
15
src/server/service/schema.ts
Normal file
15
src/server/service/schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { parsedCommandSchema } from "./parseCommandXml";
|
||||
|
||||
export const projectMetaSchema = z.object({
|
||||
projectName: z.string().nullable(),
|
||||
projectPath: z.string().nullable(),
|
||||
lastModifiedAt: z.string().nullable(),
|
||||
sessionCount: z.number(),
|
||||
});
|
||||
|
||||
export const sessionMetaSchema = z.object({
|
||||
messageCount: z.number(),
|
||||
firstCommand: parsedCommandSchema.nullable(),
|
||||
lastModifiedAt: z.string().nullable(),
|
||||
});
|
||||
69
src/server/service/session/SessionRepository.ts
Normal file
69
src/server/service/session/SessionRepository.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { parseJsonl } from "../parseJsonl";
|
||||
import { decodeProjectId } from "../project/id";
|
||||
import type { Session, SessionDetail } from "../types";
|
||||
import { decodeSessionId, encodeSessionId } from "./id";
|
||||
import { sessionMetaStorage } from "./sessionMetaStorage";
|
||||
|
||||
const getTime = (date: string | null) => {
|
||||
if (date === null) return 0;
|
||||
return new Date(date).getTime();
|
||||
};
|
||||
|
||||
export class SessionRepository {
|
||||
public async getSession(
|
||||
projectId: string,
|
||||
sessionId: string,
|
||||
): Promise<{
|
||||
session: SessionDetail;
|
||||
}> {
|
||||
const sessionPath = decodeSessionId(projectId, sessionId);
|
||||
const content = await readFile(sessionPath, "utf-8");
|
||||
|
||||
const conversations = parseJsonl(content);
|
||||
|
||||
const sessionDetail: SessionDetail = {
|
||||
id: sessionId,
|
||||
jsonlFilePath: sessionPath,
|
||||
meta: await sessionMetaStorage.getSessionMeta(projectId, sessionId),
|
||||
conversations,
|
||||
};
|
||||
|
||||
return {
|
||||
session: sessionDetail,
|
||||
};
|
||||
}
|
||||
|
||||
public async getSessions(
|
||||
projectId: string,
|
||||
): Promise<{ sessions: Session[] }> {
|
||||
try {
|
||||
const claudeProjectPath = decodeProjectId(projectId);
|
||||
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
|
||||
const sessions = await Promise.all(
|
||||
dirents
|
||||
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
|
||||
.map(async (d) => ({
|
||||
id: encodeSessionId(resolve(claudeProjectPath, d.name)),
|
||||
jsonlFilePath: resolve(claudeProjectPath, d.name),
|
||||
meta: await sessionMetaStorage.getSessionMeta(
|
||||
projectId,
|
||||
encodeSessionId(resolve(claudeProjectPath, d.name)),
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
sessions: sessions.sort((a, b) => {
|
||||
return (
|
||||
getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt)
|
||||
);
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read sessions for project ${projectId}:`, error);
|
||||
return { sessions: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { parseJsonl } from "../parseJsonl";
|
||||
import { decodeProjectId } from "../project/id";
|
||||
import type { SessionDetail } from "../types";
|
||||
import { getSessionMeta } from "./getSessionMeta";
|
||||
|
||||
export const getSession = async (
|
||||
projectId: string,
|
||||
sessionId: string,
|
||||
): Promise<{
|
||||
session: SessionDetail;
|
||||
}> => {
|
||||
const projectPath = decodeProjectId(projectId);
|
||||
const sessionPath = resolve(projectPath, `${sessionId}.jsonl`);
|
||||
|
||||
const content = await readFile(sessionPath, "utf-8");
|
||||
|
||||
const conversations = parseJsonl(content);
|
||||
|
||||
const sessionDetail: SessionDetail = {
|
||||
id: sessionId,
|
||||
jsonlFilePath: sessionPath,
|
||||
meta: await getSessionMeta(sessionPath),
|
||||
conversations,
|
||||
};
|
||||
|
||||
return {
|
||||
session: sessionDetail,
|
||||
};
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { type ParsedCommand, parseCommandXml } from "../parseCommandXml";
|
||||
import { parseJsonl } from "../parseJsonl";
|
||||
import type { SessionMeta } from "../types";
|
||||
|
||||
const firstCommandCache = new Map<string, ParsedCommand | null>();
|
||||
|
||||
const ignoreCommands = [
|
||||
"/clear",
|
||||
"/login",
|
||||
"/logout",
|
||||
"/exit",
|
||||
"/mcp",
|
||||
"/memory",
|
||||
];
|
||||
|
||||
const getFirstCommand = (
|
||||
jsonlFilePath: string,
|
||||
lines: string[],
|
||||
): ParsedCommand | null => {
|
||||
const cached = 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) {
|
||||
firstCommandCache.set(jsonlFilePath, firstCommand);
|
||||
}
|
||||
|
||||
return firstCommand;
|
||||
};
|
||||
|
||||
export const getSessionMeta = async (
|
||||
jsonlFilePath: string,
|
||||
): Promise<SessionMeta> => {
|
||||
const stats = statSync(jsonlFilePath);
|
||||
const lastModifiedUnixTime = stats.ctime.getTime();
|
||||
|
||||
const content = await readFile(jsonlFilePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
const sessionMeta: SessionMeta = {
|
||||
messageCount: lines.length,
|
||||
firstCommand: getFirstCommand(jsonlFilePath, lines),
|
||||
lastModifiedAt: lastModifiedUnixTime
|
||||
? new Date(lastModifiedUnixTime).toISOString()
|
||||
: null,
|
||||
};
|
||||
|
||||
return sessionMeta;
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { basename, extname, resolve } from "node:path";
|
||||
|
||||
import { decodeProjectId } from "../project/id";
|
||||
import type { Session } from "../types";
|
||||
import { getSessionMeta } from "./getSessionMeta";
|
||||
|
||||
const getTime = (date: string | null) => {
|
||||
if (date === null) return 0;
|
||||
return new Date(date).getTime();
|
||||
};
|
||||
|
||||
export const getSessions = async (
|
||||
projectId: string,
|
||||
): Promise<{ sessions: Session[] }> => {
|
||||
const claudeProjectPath = decodeProjectId(projectId);
|
||||
|
||||
try {
|
||||
const dirents = await readdir(claudeProjectPath, { withFileTypes: true });
|
||||
const sessions = await Promise.all(
|
||||
dirents
|
||||
.filter((d) => d.isFile() && d.name.endsWith(".jsonl"))
|
||||
.map(async (d): Promise<Session> => {
|
||||
const fullPath = resolve(claudeProjectPath, d.name);
|
||||
|
||||
return {
|
||||
id: basename(fullPath, extname(fullPath)),
|
||||
jsonlFilePath: fullPath,
|
||||
meta: await getSessionMeta(fullPath),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
sessions: sessions.sort((a, b) => {
|
||||
return getTime(b.meta.lastModifiedAt) - getTime(a.meta.lastModifiedAt);
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read sessions for project ${projectId}:`, error);
|
||||
return { sessions: [] };
|
||||
}
|
||||
};
|
||||
11
src/server/service/session/id.ts
Normal file
11
src/server/service/session/id.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { basename, extname, resolve } from "node:path";
|
||||
import { decodeProjectId } from "../project/id";
|
||||
|
||||
export const encodeSessionId = (jsonlFilePath: string) => {
|
||||
return basename(jsonlFilePath, extname(jsonlFilePath));
|
||||
};
|
||||
|
||||
export const decodeSessionId = (projectId: string, sessionId: string) => {
|
||||
const projectPath = decodeProjectId(projectId);
|
||||
return resolve(projectPath, `${sessionId}.jsonl`);
|
||||
};
|
||||
131
src/server/service/session/sessionMetaStorage.ts
Normal file
131
src/server/service/session/sessionMetaStorage.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
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();
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { z } from "zod";
|
||||
import type { Conversation } from "../../lib/conversation-schema";
|
||||
import type { ParsedCommand } from "./parseCommandXml";
|
||||
import type { projectMetaSchema, sessionMetaSchema } from "./schema";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@@ -7,12 +8,7 @@ export type Project = {
|
||||
meta: ProjectMeta;
|
||||
};
|
||||
|
||||
export type ProjectMeta = {
|
||||
projectName: string | null;
|
||||
projectPath: string | null;
|
||||
lastModifiedAt: Date | null;
|
||||
sessionCount: number;
|
||||
};
|
||||
export type ProjectMeta = z.infer<typeof projectMetaSchema>;
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
@@ -20,11 +16,7 @@ export type Session = {
|
||||
meta: SessionMeta;
|
||||
};
|
||||
|
||||
export type SessionMeta = {
|
||||
messageCount: number;
|
||||
firstCommand: ParsedCommand | null;
|
||||
lastModifiedAt: string | null;
|
||||
};
|
||||
export type SessionMeta = z.infer<typeof sessionMetaSchema>;
|
||||
|
||||
export type ErrorJsonl = {
|
||||
type: "x-error";
|
||||
|
||||
Reference in New Issue
Block a user