diff --git a/src/extensions/shared/workspace/index.ts b/src/extensions/shared/workspace/index.ts new file mode 100644 index 0000000..a21a9fb --- /dev/null +++ b/src/extensions/shared/workspace/index.ts @@ -0,0 +1,97 @@ +import { createHash } from "node:crypto"; +import { appendFile, mkdir, readFile, 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; + append(role: MessageRole, content: string): Promise; +} + +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 conversationId = options.conversationId ?? "default"; + const workspaceDirectory = join(home, "workspaces", workspaceId); + const conversationDirectory = join(workspaceDirectory, "conversations", conversationId); + const messagesFile = join(conversationDirectory, "messages.jsonl"); + + const workspace: WorkspaceService = { + home, + projectPath, + workspaceId, + conversationId, + + async messages() { + try { + const content = await readFile(messagesFile, "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(messagesFile, `${JSON.stringify(message)}\n`, "utf8"); + return message; + }, + }; + + return { + id: ExtensionId.Workspace, + + async setup(context) { + await mkdir(conversationDirectory, { recursive: true }); + await writeFile( + join(workspaceDirectory, "workspace.json"), + `${JSON.stringify( + { + id: workspaceId, + kind: "linked", + projectPath, + activeConversationId: conversationId, + }, + null, + 2, + )}\n`, + "utf8", + ); + + context.add(Hook.Workspace, workspace); + }, + }; +}