import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { resolveUserPath } from "../utils.js"; export const DEFAULT_AGENT_WORKSPACE_DIR = path.join(os.homedir(), "clawd"); export const DEFAULT_AGENTS_FILENAME = "AGENTS.md"; export const DEFAULT_SOUL_FILENAME = "SOUL.md"; export const DEFAULT_TOOLS_FILENAME = "TOOLS.md"; const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md — Clawdis Workspace This folder is the assistant’s working directory. ## Safety defaults - Don’t exfiltrate secrets or private data. - Don’t run destructive commands unless explicitly asked. - Be concise in chat; write longer output to files in this workspace. ## How to use this - Put project notes, scratch files, and “memory” here. - Customize this file with additional instructions for your assistant. `; const DEFAULT_SOUL_TEMPLATE = `# SOUL.md — Persona & Boundaries Describe who the assistant is, tone, and boundaries. - Keep replies concise and direct. - Ask clarifying questions when needed. - Never send streaming/partial replies to external messaging surfaces. `; const DEFAULT_TOOLS_TEMPLATE = `# TOOLS.md — User Tool Notes (editable) This file is for *your* notes about external tools and conventions. It does not define which tools exist; Clawdis provides built-in tools internally. ## Examples ### imsg - Send an iMessage/SMS: describe who/what, confirm before sending. - Prefer short messages; avoid sending secrets. ### sag - Text-to-speech: specify voice, target speaker/room, and whether to stream. Add whatever else you want the assistant to know about your local toolchain. `; export type WorkspaceBootstrapFileName = | typeof DEFAULT_AGENTS_FILENAME | typeof DEFAULT_SOUL_FILENAME | typeof DEFAULT_TOOLS_FILENAME; export type WorkspaceBootstrapFile = { name: WorkspaceBootstrapFileName; path: string; content?: string; missing: boolean; }; async function writeFileIfMissing(filePath: string, content: string) { try { await fs.writeFile(filePath, content, { encoding: "utf-8", flag: "wx", }); } catch (err) { const anyErr = err as { code?: string }; if (anyErr.code !== "EEXIST") throw err; } } export async function ensureAgentWorkspace(params?: { dir?: string; ensureBootstrapFiles?: boolean; }): Promise<{ dir: string; agentsPath?: string; soulPath?: string; toolsPath?: string; }> { const rawDir = params?.dir?.trim() ? params.dir.trim() : DEFAULT_AGENT_WORKSPACE_DIR; const dir = resolveUserPath(rawDir); await fs.mkdir(dir, { recursive: true }); if (!params?.ensureBootstrapFiles) return { dir }; const agentsPath = path.join(dir, DEFAULT_AGENTS_FILENAME); const soulPath = path.join(dir, DEFAULT_SOUL_FILENAME); const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME); await writeFileIfMissing(agentsPath, DEFAULT_AGENTS_TEMPLATE); await writeFileIfMissing(soulPath, DEFAULT_SOUL_TEMPLATE); await writeFileIfMissing(toolsPath, DEFAULT_TOOLS_TEMPLATE); return { dir, agentsPath, soulPath, toolsPath }; } export async function loadWorkspaceBootstrapFiles( dir: string, ): Promise { const resolvedDir = resolveUserPath(dir); const entries: Array<{ name: WorkspaceBootstrapFileName; filePath: string; }> = [ { name: DEFAULT_AGENTS_FILENAME, filePath: path.join(resolvedDir, DEFAULT_AGENTS_FILENAME), }, { name: DEFAULT_SOUL_FILENAME, filePath: path.join(resolvedDir, DEFAULT_SOUL_FILENAME), }, { name: DEFAULT_TOOLS_FILENAME, filePath: path.join(resolvedDir, DEFAULT_TOOLS_FILENAME), }, ]; const result: WorkspaceBootstrapFile[] = []; for (const entry of entries) { try { const content = await fs.readFile(entry.filePath, "utf-8"); result.push({ name: entry.name, path: entry.filePath, content, missing: false, }); } catch { result.push({ name: entry.name, path: entry.filePath, missing: true }); } } return result; }