143 lines
4.0 KiB
TypeScript
143 lines
4.0 KiB
TypeScript
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<WorkspaceBootstrapFile[]> {
|
||
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;
|
||
}
|