src / store.ts
// Paths and small synchronous file helpers. Everything throughLine keeps lives under one
// folder, and every write goes through a temporary file and a rename so a crash or a
// plugin restart can never leave a half-written file behind.
import * as crypto from "crypto";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
export function rootDir(): string {
return process.env.THROUGHLINE_HOME || path.join(os.homedir(), ".lmstudio", "throughline");
}
export const projectsDir = () => path.join(rootDir(), "projects");
export const chatsDir = () => path.join(rootDir(), "chats");
export const trashDir = () => path.join(rootDir(), "trash");
export const projectDir = (slug: string) => path.join(projectsDir(), slug);
export const chatDir = (chatId: string) => path.join(chatsDir(), chatId);
export function ensureDir(dir: string): void {
fs.mkdirSync(dir, { recursive: true });
}
export function writeFileAtomic(file: string, content: string): void {
ensureDir(path.dirname(file));
const tmp = `${file}.${process.pid}-${crypto.randomBytes(4).toString("hex")}.tmp`;
fs.writeFileSync(tmp, content, "utf8");
fs.renameSync(tmp, file);
}
export function writeJson(file: string, value: unknown): void {
writeFileAtomic(file, JSON.stringify(value, null, 2) + "\n");
}
export function readJson<T>(file: string): T | null {
try {
return JSON.parse(fs.readFileSync(file, "utf8")) as T;
} catch {
return null;
}
}
export function appendLine(file: string, line: string): void {
ensureDir(path.dirname(file));
fs.appendFileSync(file, line + "\n", "utf8");
}
// A path shown to the user or the model, with the home folder written as "~".
export function displayPath(p: string): string {
const home = os.homedir();
return p.toLowerCase().startsWith(home.toLowerCase()) ? "~" + p.slice(home.length) : p;
}
src / store.ts
// Paths and small synchronous file helpers. Everything throughLine keeps lives under one
// folder, and every write goes through a temporary file and a rename so a crash or a
// plugin restart can never leave a half-written file behind.
import * as crypto from "crypto";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
export function rootDir(): string {
return process.env.THROUGHLINE_HOME || path.join(os.homedir(), ".lmstudio", "throughline");
}
export const projectsDir = () => path.join(rootDir(), "projects");
export const chatsDir = () => path.join(rootDir(), "chats");
export const trashDir = () => path.join(rootDir(), "trash");
export const projectDir = (slug: string) => path.join(projectsDir(), slug);
export const chatDir = (chatId: string) => path.join(chatsDir(), chatId);
export function ensureDir(dir: string): void {
fs.mkdirSync(dir, { recursive: true });
}
export function writeFileAtomic(file: string, content: string): void {
ensureDir(path.dirname(file));
const tmp = `${file}.${process.pid}-${crypto.randomBytes(4).toString("hex")}.tmp`;
fs.writeFileSync(tmp, content, "utf8");
fs.renameSync(tmp, file);
}
export function writeJson(file: string, value: unknown): void {
writeFileAtomic(file, JSON.stringify(value, null, 2) + "\n");
}
export function readJson<T>(file: string): T | null {
try {
return JSON.parse(fs.readFileSync(file, "utf8")) as T;
} catch {
return null;
}
}
export function appendLine(file: string, line: string): void {
ensureDir(path.dirname(file));
fs.appendFileSync(file, line + "\n", "utf8");
}
// A path shown to the user or the model, with the home folder written as "~".
export function displayPath(p: string): string {
const home = os.homedir();
return p.toLowerCase().startsWith(home.toLowerCase()) ? "~" + p.slice(home.length) : p;
}