Files
platform/src/servers/api/pi/storage.ts
T

461 lines
12 KiB
TypeScript

import * as fs from "fs/promises";
import * as path from "path";
import type { SessionMeta, Message, GroupMeta } from "./types";
const PI_SESSIONS_DIR = ".pi-sessions";
const GROUP_PREFIX = "@";
const GROUP_META_FILE = ".group-meta.json";
function getSessionDir(cwd: string, sessionId: string, groupSlug?: string | null): string {
if (groupSlug) {
return path.join(cwd, PI_SESSIONS_DIR, `${GROUP_PREFIX}${groupSlug}`, sessionId);
}
return path.join(cwd, PI_SESSIONS_DIR, sessionId);
}
function getMetaPath(cwd: string, sessionId: string, groupSlug?: string | null): string {
return path.join(getSessionDir(cwd, sessionId, groupSlug), "meta.json");
}
function getMessagesPath(cwd: string, sessionId: string, groupSlug?: string | null): string {
return path.join(getSessionDir(cwd, sessionId, groupSlug), "messages.json");
}
function getGroupDir(cwd: string, groupSlug: string): string {
return path.join(cwd, PI_SESSIONS_DIR, `${GROUP_PREFIX}${groupSlug}`);
}
function getGroupMetaPath(cwd: string, groupSlug: string): string {
return path.join(getGroupDir(cwd, groupSlug), GROUP_META_FILE);
}
async function isGroupDirectory(dirPath: string): Promise<boolean> {
try {
const groupMetaPath = path.join(dirPath, GROUP_META_FILE);
await fs.access(groupMetaPath);
return true;
} catch {
return false;
}
}
export async function saveSession(
cwd: string,
sessionId: string,
meta: SessionMeta,
messages: Message[]
): Promise<void> {
const sessionDir = getSessionDir(cwd, sessionId, meta.groupSlug);
await fs.mkdir(sessionDir, { recursive: true });
const metaPath = getMetaPath(cwd, sessionId, meta.groupSlug);
const messagesPath = getMessagesPath(cwd, sessionId, meta.groupSlug);
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
await fs.writeFile(
messagesPath,
JSON.stringify({ messages }, null, 2)
);
}
export async function loadSession(
cwd: string,
sessionId: string,
groupSlug?: string | null
): Promise<{ meta: SessionMeta; messages: Message[] }> {
const metaPath = getMetaPath(cwd, sessionId, groupSlug);
const messagesPath = getMessagesPath(cwd, sessionId, groupSlug);
const metaContent = await fs.readFile(metaPath, "utf-8");
const messagesContent = await fs.readFile(messagesPath, "utf-8");
const meta: SessionMeta = JSON.parse(metaContent);
const { messages } = JSON.parse(messagesContent) as {
messages: Message[];
};
return { meta, messages };
}
export async function sessionExists(
cwd: string,
sessionId: string,
groupSlug?: string | null
): Promise<boolean> {
try {
const sessionDir = getSessionDir(cwd, sessionId, groupSlug);
await fs.access(sessionDir);
return true;
} catch {
return false;
}
}
export async function updateSessionMeta(
cwd: string,
sessionId: string,
updates: Partial<SessionMeta>,
groupSlug?: string | null
): Promise<SessionMeta> {
const { meta, messages } = await loadSession(cwd, sessionId, groupSlug);
const updatedMeta: SessionMeta = {
...meta,
...updates,
updatedAt: Date.now(),
};
await saveSession(cwd, sessionId, updatedMeta, messages);
return updatedMeta;
}
export async function deleteSession(
cwd: string,
sessionId: string,
groupSlug?: string | null
): Promise<void> {
const sessionDir = getSessionDir(cwd, sessionId, groupSlug);
await fs.rm(sessionDir, { recursive: true, force: true });
}
export async function listUserSessions(
baseCwd: string
): Promise<SessionMeta[]> {
const sessionsDir = path.join(baseCwd, PI_SESSIONS_DIR);
try {
await fs.access(sessionsDir);
} catch {
return [];
}
const entries = await fs.readdir(sessionsDir);
const sessions: SessionMeta[] = [];
for (const entry of entries) {
const entryPath = path.join(sessionsDir, entry);
const stats = await fs.stat(entryPath);
if (!stats.isDirectory()) continue;
// Check if it's a group (starts with @)
if (entry.startsWith(GROUP_PREFIX)) {
const groupSlug = entry.slice(GROUP_PREFIX.length);
const groupSessions = await fs.readdir(entryPath);
for (const sessionId of groupSessions) {
if (sessionId === GROUP_META_FILE) continue;
try {
const metaPath = getMetaPath(baseCwd, sessionId, groupSlug);
const metaContent = await fs.readFile(metaPath, "utf-8");
const meta: SessionMeta = JSON.parse(metaContent);
sessions.push(meta);
} catch {
continue;
}
}
} else {
// Regular ungrouped session
try {
const metaPath = getMetaPath(baseCwd, entry);
const metaContent = await fs.readFile(metaPath, "utf-8");
const meta: SessionMeta = JSON.parse(metaContent);
sessions.push(meta);
} catch {
continue;
}
}
}
sessions.sort((a, b) => b.updatedAt - a.updatedAt);
return sessions;
}
export async function searchSessions(
baseCwd: string,
query: string
): Promise<
Array<SessionMeta & { preview?: string; relevance?: number }>
> {
const sessionsDir = path.join(baseCwd, PI_SESSIONS_DIR);
try {
await fs.access(sessionsDir);
} catch {
return [];
}
const entries = await fs.readdir(sessionsDir);
const results: Array<
SessionMeta & { preview?: string; relevance?: number }
> = [];
const lowerQuery = query.toLowerCase();
for (const entry of entries) {
const entryPath = path.join(sessionsDir, entry);
const stats = await fs.stat(entryPath);
if (!stats.isDirectory()) continue;
// Check if it's a group
if (entry.startsWith(GROUP_PREFIX)) {
const groupSlug = entry.slice(GROUP_PREFIX.length);
// Search in group metadata
try {
const groupMetaPath = getGroupMetaPath(baseCwd, groupSlug);
const groupMetaContent = await fs.readFile(groupMetaPath, "utf-8");
const groupMeta: GroupMeta = JSON.parse(groupMetaContent);
let groupRelevance = 0;
if (groupMeta.name.toLowerCase().includes(lowerQuery)) {
groupRelevance += 2.0;
}
if (groupMeta.description?.toLowerCase().includes(lowerQuery)) {
groupRelevance += 1.5;
}
// Search sessions in group
const groupSessions = await fs.readdir(entryPath);
for (const sessionId of groupSessions) {
if (sessionId === GROUP_META_FILE) continue;
try {
const { meta, messages } = await loadSession(baseCwd, sessionId, groupSlug);
let relevance = groupRelevance;
let preview = "";
if (meta.title.toLowerCase().includes(lowerQuery)) {
relevance += 1.0;
preview = meta.title;
}
for (const msg of messages) {
if (msg.text && msg.text.toLowerCase().includes(lowerQuery)) {
relevance += 0.5;
if (!preview) {
const index = msg.text.toLowerCase().indexOf(lowerQuery);
const start = Math.max(0, index - 50);
const end = Math.min(msg.text.length, index + query.length + 50);
preview = "..." + msg.text.slice(start, end) + "...";
}
}
}
if (relevance > 0) {
results.push({ ...meta, preview, relevance });
}
} catch {
continue;
}
}
} catch {
continue;
}
} else {
// Regular ungrouped session
try {
const { meta, messages } = await loadSession(baseCwd, entry);
let relevance = 0;
let preview = "";
if (meta.title.toLowerCase().includes(lowerQuery)) {
relevance += 1.0;
preview = meta.title;
}
for (const msg of messages) {
if (msg.text && msg.text.toLowerCase().includes(lowerQuery)) {
relevance += 0.5;
if (!preview) {
const index = msg.text.toLowerCase().indexOf(lowerQuery);
const start = Math.max(0, index - 50);
const end = Math.min(msg.text.length, index + query.length + 50);
preview = "..." + msg.text.slice(start, end) + "...";
}
}
}
if (relevance > 0) {
results.push({ ...meta, preview, relevance });
}
} catch {
continue;
}
}
}
results.sort((a, b) => (b.relevance || 0) - (a.relevance || 0));
return results;
}
/**
* Group Management Functions
*/
export async function saveGroup(
cwd: string,
groupMeta: GroupMeta
): Promise<void> {
const groupDir = getGroupDir(cwd, groupMeta.slug);
await fs.mkdir(groupDir, { recursive: true });
const groupMetaPath = getGroupMetaPath(cwd, groupMeta.slug);
await fs.writeFile(groupMetaPath, JSON.stringify(groupMeta, null, 2));
}
export async function loadGroup(
cwd: string,
groupSlug: string
): Promise<GroupMeta> {
const groupMetaPath = getGroupMetaPath(cwd, groupSlug);
const groupMetaContent = await fs.readFile(groupMetaPath, "utf-8");
return JSON.parse(groupMetaContent) as GroupMeta;
}
export async function groupExists(
cwd: string,
groupSlug: string
): Promise<boolean> {
try {
const groupMetaPath = getGroupMetaPath(cwd, groupSlug);
await fs.access(groupMetaPath);
return true;
} catch {
return false;
}
}
export async function listGroups(
baseCwd: string
): Promise<GroupMeta[]> {
const sessionsDir = path.join(baseCwd, PI_SESSIONS_DIR);
try {
await fs.access(sessionsDir);
} catch {
return [];
}
const entries = await fs.readdir(sessionsDir);
const groups: GroupMeta[] = [];
for (const entry of entries) {
if (!entry.startsWith(GROUP_PREFIX)) continue;
const groupSlug = entry.slice(GROUP_PREFIX.length);
try {
const groupMeta = await loadGroup(baseCwd, groupSlug);
groups.push(groupMeta);
} catch {
continue;
}
}
groups.sort((a, b) => b.updatedAt - a.updatedAt);
return groups;
}
export async function updateGroupMeta(
cwd: string,
groupSlug: string,
updates: Partial<GroupMeta>
): Promise<GroupMeta> {
const groupMeta = await loadGroup(cwd, groupSlug);
const updatedGroupMeta: GroupMeta = {
...groupMeta,
...updates,
slug: groupMeta.slug, // Prevent slug changes
updatedAt: Date.now(),
};
await saveGroup(cwd, updatedGroupMeta);
return updatedGroupMeta;
}
export async function deleteGroup(
cwd: string,
groupSlug: string
): Promise<void> {
const groupDir = getGroupDir(cwd, groupSlug);
// Move all sessions in the group to root level
try {
const entries = await fs.readdir(groupDir);
const sessionsDir = path.join(cwd, PI_SESSIONS_DIR);
for (const entry of entries) {
if (entry === GROUP_META_FILE) continue;
const sessionDir = path.join(groupDir, entry);
const destDir = path.join(sessionsDir, entry);
// Update session meta to remove groupSlug
try {
const metaPath = path.join(sessionDir, "meta.json");
const metaContent = await fs.readFile(metaPath, "utf-8");
const meta: SessionMeta = JSON.parse(metaContent);
meta.groupSlug = null;
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
} catch {
// Continue even if meta update fails
}
// Move session directory
await fs.rename(sessionDir, destDir);
}
} catch (err) {
// Continue to delete group even if moving fails
}
// Delete the group directory
await fs.rm(groupDir, { recursive: true, force: true });
}
export async function moveSession(
cwd: string,
sessionId: string,
fromGroupSlug: string | null,
toGroupSlug: string | null
): Promise<SessionMeta> {
// Load the session
const { meta, messages } = await loadSession(cwd, sessionId, fromGroupSlug);
// Update groupSlug
meta.groupSlug = toGroupSlug;
meta.updatedAt = Date.now();
// Save to new location
await saveSession(cwd, sessionId, meta, messages);
// Delete from old location
await deleteSession(cwd, sessionId, fromGroupSlug);
// Update session count in groups
if (fromGroupSlug) {
try {
const fromGroup = await loadGroup(cwd, fromGroupSlug);
fromGroup.sessionCount = Math.max(0, fromGroup.sessionCount - 1);
fromGroup.updatedAt = Date.now();
await saveGroup(cwd, fromGroup);
} catch {
// Group might not exist
}
}
if (toGroupSlug) {
try {
const toGroup = await loadGroup(cwd, toGroupSlug);
toGroup.sessionCount += 1;
toGroup.updatedAt = Date.now();
await saveGroup(cwd, toGroup);
} catch {
// Group might not exist
}
}
return meta;
}