149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
import { homedir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
|
|
import type { Extension } from "../../../kernel";
|
|
import { ExtensionId, Hook } from "../../catalog";
|
|
|
|
export type MessageRole = "system" | "user" | "assistant";
|
|
|
|
export interface ChatMessage {
|
|
role: MessageRole;
|
|
content: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface WorkspaceService {
|
|
home: string;
|
|
projectPath: string;
|
|
workspaceId: string;
|
|
conversationId: string;
|
|
messages(): Promise<ChatMessage[]>;
|
|
append(role: MessageRole, content: string): Promise<ChatMessage>;
|
|
newConversation(): Promise<string>;
|
|
switchConversation(conversationId: string): Promise<boolean>;
|
|
}
|
|
|
|
export interface WorkspaceOptions {
|
|
home?: string;
|
|
projectPath?: string;
|
|
conversationId?: string;
|
|
}
|
|
|
|
export function createWorkspaceExtension(options: WorkspaceOptions = {}): Extension {
|
|
const home = resolve(
|
|
options.home ?? process.env.LLM_TO_AGENT_HOME ?? join(homedir(), ".llm-to-agent"),
|
|
);
|
|
const projectPath = resolve(options.projectPath ?? process.cwd());
|
|
const workspaceId = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
|
|
const workspaceDirectory = join(home, "workspaces", workspaceId);
|
|
const conversationsDirectory = join(workspaceDirectory, "conversations");
|
|
const workspaceFile = join(workspaceDirectory, "workspace.json");
|
|
let conversationId = options.conversationId ?? "default";
|
|
|
|
async function saveWorkspace() {
|
|
await writeFile(
|
|
workspaceFile,
|
|
`${JSON.stringify(
|
|
{
|
|
id: workspaceId,
|
|
kind: "linked",
|
|
projectPath,
|
|
activeConversationId: conversationId,
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
const workspace: WorkspaceService = {
|
|
home,
|
|
projectPath,
|
|
workspaceId,
|
|
get conversationId() {
|
|
return conversationId;
|
|
},
|
|
|
|
async messages() {
|
|
try {
|
|
const content = await readFile(
|
|
join(conversationsDirectory, conversationId, "messages.jsonl"),
|
|
"utf8",
|
|
);
|
|
return content
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line) as ChatMessage);
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async append(role, content) {
|
|
const message = {
|
|
role,
|
|
content,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
|
|
await appendFile(
|
|
join(conversationsDirectory, conversationId, "messages.jsonl"),
|
|
`${JSON.stringify(message)}\n`,
|
|
"utf8",
|
|
);
|
|
return message;
|
|
},
|
|
|
|
async newConversation() {
|
|
const newConversationId = randomUUID();
|
|
await mkdir(join(conversationsDirectory, newConversationId), { recursive: true });
|
|
conversationId = newConversationId;
|
|
await saveWorkspace();
|
|
return conversationId;
|
|
},
|
|
|
|
async switchConversation(nextConversationId) {
|
|
const conversations = await readdir(conversationsDirectory, {
|
|
withFileTypes: true,
|
|
});
|
|
const exists = conversations.some(
|
|
(entry) => entry.isDirectory() && entry.name === nextConversationId,
|
|
);
|
|
|
|
if (!exists) return false;
|
|
|
|
conversationId = nextConversationId;
|
|
await saveWorkspace();
|
|
return true;
|
|
},
|
|
};
|
|
|
|
return {
|
|
id: ExtensionId.Workspace,
|
|
|
|
async setup(context) {
|
|
await mkdir(workspaceDirectory, { recursive: true });
|
|
|
|
if (!options.conversationId) {
|
|
try {
|
|
const savedWorkspace = JSON.parse(await readFile(workspaceFile, "utf8"));
|
|
if (savedWorkspace.activeConversationId) {
|
|
conversationId = savedWorkspace.activeConversationId;
|
|
}
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
}
|
|
}
|
|
|
|
await mkdir(join(conversationsDirectory, conversationId), { recursive: true });
|
|
await saveWorkspace();
|
|
|
|
context.add(Hook.Workspace, workspace);
|
|
},
|
|
};
|
|
}
|