diff --git a/apps/macos/Sources/Clawdis/AgentWorkspace.swift b/apps/macos/Sources/Clawdis/AgentWorkspace.swift index d70107491..394b3ff91 100644 --- a/apps/macos/Sources/Clawdis/AgentWorkspace.swift +++ b/apps/macos/Sources/Clawdis/AgentWorkspace.swift @@ -4,6 +4,8 @@ import OSLog enum AgentWorkspace { private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "workspace") static let agentsFilename = "AGENTS.md" + static let soulFilename = "SOUL.md" + private static let templateDirname = "templates" static let identityStartMarker = "" static let identityEndMarker = "" @@ -35,6 +37,11 @@ enum AgentWorkspace { try self.defaultTemplate().write(to: agentsURL, atomically: true, encoding: .utf8) self.logger.info("Created AGENTS.md at \(agentsURL.path, privacy: .public)") } + let soulURL = workspaceURL.appendingPathComponent(self.soulFilename) + if !FileManager.default.fileExists(atPath: soulURL.path) { + try self.defaultSoulTemplate().write(to: soulURL, atomically: true, encoding: .utf8) + self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)") + } return agentsURL } @@ -64,13 +71,13 @@ enum AgentWorkspace { } static func defaultTemplate() -> String { - """ - # AGENTS.md — Clawdis Workspace + let fallback = """ + # AGENTS.md - Clawdis Workspace - This folder is the assistant’s working directory. + This folder is the assistant's working directory. ## Backup tip (recommended) - If you treat this workspace as the agent’s “memory”, make it a git repo (ideally private) so your identity + If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity and notes are backed up. ```bash @@ -80,13 +87,94 @@ enum AgentWorkspace { ``` ## Safety defaults - - Don’t exfiltrate secrets or private data. - - Don’t run destructive commands unless explicitly asked. + - 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. + ## Daily memory (recommended) + - Keep a short daily log at memory/YYYY-MM-DD.md (create memory/ if needed). + - On session start, read today + yesterday if present. + - Capture durable facts, preferences, and decisions; avoid secrets. + ## Customize - - Add your preferred style, rules, and “memory” here. + - Add your preferred style, rules, and "memory" here. """ + return self.loadTemplate(named: self.agentsFilename, fallback: fallback) + } + + static func defaultSoulTemplate() -> String { + let fallback = """ + # 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. + """ + return self.loadTemplate(named: self.soulFilename, fallback: fallback) + } + + private static func loadTemplate(named: String, fallback: String) -> String { + for url in self.templateURLs(named: named) { + if let content = try? String(contentsOf: url, encoding: .utf8) { + let stripped = self.stripFrontMatter(content) + if !stripped.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return stripped + } + } + } + return fallback + } + + private static func templateURLs(named: String) -> [URL] { + var urls: [URL] = [] + if let resource = Bundle.main.url( + forResource: named.replacingOccurrences(of: ".md", with: ""), + withExtension: "md", + subdirectory: self.templateDirname) + { + urls.append(resource) + } + if let resource = Bundle.main.url( + forResource: named, + withExtension: nil, + subdirectory: self.templateDirname) + { + urls.append(resource) + } + if let dev = self.devTemplateURL(named: named) { + urls.append(dev) + } + let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + urls.append(cwd.appendingPathComponent("docs") + .appendingPathComponent(self.templateDirname) + .appendingPathComponent(named)) + return urls + } + + private static func devTemplateURL(named: String) -> URL? { + let sourceURL = URL(fileURLWithPath: #filePath) + let repoRoot = sourceURL + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + return repoRoot.appendingPathComponent("docs") + .appendingPathComponent(self.templateDirname) + .appendingPathComponent(named) + } + + private static func stripFrontMatter(_ content: String) -> String { + guard content.hasPrefix("---") else { return content } + let start = content.index(content.startIndex, offsetBy: 3) + guard let range = content.range(of: "\n---", range: start.. String { diff --git a/docs/AGENTS.default.md b/docs/AGENTS.default.md index c4a593de2..45159291d 100644 --- a/docs/AGENTS.default.md +++ b/docs/AGENTS.default.md @@ -16,13 +16,21 @@ Clawdis uses a dedicated workspace directory for the agent. Default: `~/.clawdis mkdir -p ~/.clawdis/workspace ``` -2) Copy this template into the workspace as `AGENTS.md` (overwrites any existing file): +2) Copy the default workspace templates into the workspace: + +```bash +cp docs/templates/AGENTS.md ~/.clawdis/workspace/AGENTS.md +cp docs/templates/SOUL.md ~/.clawdis/workspace/SOUL.md +cp docs/templates/TOOLS.md ~/.clawdis/workspace/TOOLS.md +``` + +3) Optional: if you want the personal assistant tool roster, replace AGENTS.md with this file: ```bash cp docs/AGENTS.default.md ~/.clawdis/workspace/AGENTS.md ``` -3) Optional: choose a different workspace by setting `inbound.workspace` (supports `~`): +4) Optional: choose a different workspace by setting `inbound.workspace` (supports `~`): ```json5 { @@ -37,6 +45,11 @@ cp docs/AGENTS.default.md ~/.clawdis/workspace/AGENTS.md - Don’t run destructive commands unless explicitly asked. - Don’t send partial/streaming replies to external messaging surfaces (only final replies). +## Daily memory (recommended) +- Keep a short daily log at `memory/YYYY-MM-DD.md` (create `memory/` if needed). +- On session start, read today + yesterday if present. +- Capture durable facts, preferences, and decisions; avoid secrets. + ## Backup tip (recommended) If you treat this workspace as Clawd’s “memory”, make it a git repo (ideally private) so `AGENTS.md` and your memory files are backed up. diff --git a/docs/index.md b/docs/index.md index 6d93c40ac..7e77e96c8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,7 +116,7 @@ Example: - Start here: - [Configuration](./configuration.md) - [Clawd personal assistant setup](./clawd.md) - - [AGENTS.md template (default)](./AGENTS.default.md) + - [Workspace templates](./templates/AGENTS.md) - [Gateway runbook](./gateway.md) - [Nodes (iOS/Android)](./nodes.md) - [Web surfaces (Control UI)](./web.md) diff --git a/docs/onboarding.md b/docs/onboarding.md index 0d5914cbb..19f2d9304 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -106,6 +106,11 @@ git add AGENTS.md git commit -m "Add agent workspace" ``` +Daily memory lives under `memory/` in the workspace: +- one file per day: `memory/YYYY-MM-DD.md` +- read today + yesterday on session start +- keep it short (durable facts, preferences, decisions; avoid secrets) + ## Remote mode note (why OAuth is hidden) If the Gateway runs on another machine, the Anthropic OAuth credentials must be created/stored on that host (where Pi runs). diff --git a/docs/templates/AGENTS.md b/docs/templates/AGENTS.md new file mode 100644 index 000000000..df5f118c8 --- /dev/null +++ b/docs/templates/AGENTS.md @@ -0,0 +1,31 @@ +--- +summary: "Workspace template for AGENTS.md" +read_when: + - Bootstrapping a workspace manually +--- +# AGENTS.md - Clawdis Workspace + +This folder is the assistant's working directory. + +## Backup tip (recommended) +If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity +and notes are backed up. + +```bash +git init +git add AGENTS.md +git commit -m "Add agent workspace" +``` + +## 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. + +## Daily memory (recommended) +- Keep a short daily log at memory/YYYY-MM-DD.md (create memory/ if needed). +- On session start, read today + yesterday if present. +- Capture durable facts, preferences, and decisions; avoid secrets. + +## Customize +- Add your preferred style, rules, and "memory" here. diff --git a/docs/templates/SOUL.md b/docs/templates/SOUL.md new file mode 100644 index 000000000..b15e1d72a --- /dev/null +++ b/docs/templates/SOUL.md @@ -0,0 +1,12 @@ +--- +summary: "Workspace template for SOUL.md" +read_when: + - Bootstrapping a workspace manually +--- +# 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. diff --git a/docs/templates/TOOLS.md b/docs/templates/TOOLS.md new file mode 100644 index 000000000..663427b15 --- /dev/null +++ b/docs/templates/TOOLS.md @@ -0,0 +1,20 @@ +--- +summary: "Workspace template for TOOLS.md" +read_when: + - Bootstrapping a workspace manually +--- +# 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. diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index ccf672641..6da523703 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { resolveUserPath } from "../utils.js"; @@ -9,21 +10,35 @@ 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 +const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md - Clawdis Workspace -This folder is the assistant’s working directory. +This folder is the assistant's working directory. + +## Backup tip (recommended) +If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity +and notes are backed up. + +\`\`\`bash +git init +git add AGENTS.md +git commit -m "Add agent workspace" +\`\`\` ## Safety defaults -- Don’t exfiltrate secrets or private data. -- Don’t run destructive commands unless explicitly asked. +- 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. +## Daily memory (recommended) +- Keep a short daily log at memory/YYYY-MM-DD.md (create memory/ if needed). +- On session start, read today + yesterday if present. +- Capture durable facts, preferences, and decisions; avoid secrets. + +## Customize +- Add your preferred style, rules, and "memory" here. `; -const DEFAULT_SOUL_TEMPLATE = `# SOUL.md — Persona & Boundaries +const DEFAULT_SOUL_TEMPLATE = `# SOUL.md - Persona & Boundaries Describe who the assistant is, tone, and boundaries. @@ -32,7 +47,7 @@ Describe who the assistant is, tone, and boundaries. - Never send streaming/partial replies to external messaging surfaces. `; -const DEFAULT_TOOLS_TEMPLATE = `# TOOLS.md — User Tool Notes (editable) +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. @@ -49,6 +64,34 @@ It does not define which tools exist; Clawdis provides built-in tools internally Add whatever else you want the assistant to know about your local toolchain. `; +const TEMPLATE_DIR = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../docs/templates", +); + +function stripFrontMatter(content: string): string { + if (!content.startsWith("---")) return content; + const endIndex = content.indexOf("\n---", 3); + if (endIndex === -1) return content; + const start = endIndex + "\n---".length; + let trimmed = content.slice(start); + trimmed = trimmed.replace(/^\s+/, ""); + return trimmed; +} + +async function loadTemplate( + name: string, + fallback: string, +): Promise { + const templatePath = path.join(TEMPLATE_DIR, name); + try { + const content = await fs.readFile(templatePath, "utf-8"); + return stripFrontMatter(content); + } catch { + return fallback; + } +} + export type WorkspaceBootstrapFileName = | typeof DEFAULT_AGENTS_FILENAME | typeof DEFAULT_SOUL_FILENAME @@ -94,9 +137,22 @@ export async function ensureAgentWorkspace(params?: { 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); + const agentsTemplate = await loadTemplate( + DEFAULT_AGENTS_FILENAME, + DEFAULT_AGENTS_TEMPLATE, + ); + const soulTemplate = await loadTemplate( + DEFAULT_SOUL_FILENAME, + DEFAULT_SOUL_TEMPLATE, + ); + const toolsTemplate = await loadTemplate( + DEFAULT_TOOLS_FILENAME, + DEFAULT_TOOLS_TEMPLATE, + ); + + await writeFileIfMissing(agentsPath, agentsTemplate); + await writeFileIfMissing(soulPath, soulTemplate); + await writeFileIfMissing(toolsPath, toolsTemplate); return { dir, agentsPath, soulPath, toolsPath }; }