import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { getGlobalToolsDir, getUserToolsDir, getGlobalSkillsDir, getUserSkillsDir, getGlobalTasksDir, getUserTasksDir, getHomeDir, DATA_PATH, } from '@@/data-path'; type HookEntry = { type: string; command: string }; type HookRule = { matcher?: Record; hooks: HookEntry[] }; type ClaudeSettings = Record & { hooks?: Record; }; type FrontmatterEntry = { name: string; description: string }; function parseFrontmatter(content: string): Record { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return {}; const fields: Record = {}; for (const line of match[1]!.split('\n')) { const m = line.match(/^(\w+):\s*(.+)/); if (m) fields[m[1]!] = m[2]!.trim(); } return fields; } function scanDir(dir: string, metaFile: string): FrontmatterEntry[] { if (!existsSync(dir)) return []; const entries: FrontmatterEntry[] = []; for (const name of readdirSync(dir, { withFileTypes: true })) { if (!name.isDirectory()) continue; const filePath = join(dir, name.name, metaFile); if (!existsSync(filePath)) continue; const fm = parseFrontmatter(readFileSync(filePath, 'utf-8')); if (fm.name || fm.label) { entries.push({ name: fm.name ?? fm.label ?? name.name, description: fm.description ?? '' }); } } return entries; } function dedup(entries: FrontmatterEntry[]): FrontmatterEntry[] { const seen = new Map(); for (const e of entries) seen.set(e.name, e); return [...seen.values()]; } function formatList(entries: FrontmatterEntry[]): string { if (entries.length === 0) return 'None configured.\n'; return entries.map((e) => `- **${e.name}**${e.description ? ` — ${e.description}` : ''}`).join('\n') + '\n'; } export function generateContainerContext(email: string): string { const tools = dedup([...scanDir(getGlobalToolsDir(), 'TOOL.md'), ...scanDir(getUserToolsDir(email), 'TOOL.md')]); const skills = dedup([...scanDir(getGlobalSkillsDir(), 'SKILL.md'), ...scanDir(getUserSkillsDir(email), 'SKILL.md')]); const tasks = dedup([...scanDir(getGlobalTasksDir(), 'TASK.md'), ...scanDir(getUserTasksDir(email), 'TASK.md')]); const globalToolsDir = getGlobalToolsDir(); const userToolsDir = getUserToolsDir(email); const globalSkillsDir = getGlobalSkillsDir(); const userSkillsDir = getUserSkillsDir(email); const userDataDir = join(DATA_PATH, email); const content = `# Officer — User Environment This is an isolated Linux user environment managed by the Officer platform. ## Directory Layout | Path | Contents | |------|----------| | \`~\` | User home directory (read-write) | | \`~/Projects/\` | User projects | | \`~/Downloads/\` | Downloaded files | | \`${globalToolsDir}/\` | Global tools | | \`${userToolsDir}/\` | User tools | | \`${globalSkillsDir}/\` | Reference skills | | \`${userDataDir}/\` | User data (emails.db, attachments, etc.) | ## Available Tools Tools are callable capabilities used by the Officer AI agent (Pi). Each tool has a \`TOOL.md\` with documentation and an \`index.ts\` that exports an \`execute()\` function. Read individual tool docs at \`${globalToolsDir}//TOOL.md\` or \`${userToolsDir}//TOOL.md\`. ${formatList(tools)} ## Available Skills Skills are reference documentation that the AI agent uses to understand APIs and CLIs. ${formatList(skills)} ## Available Tasks Tasks are predefined instruction sets the AI agent can execute. ${formatList(tasks)} ## Creating New Tools Create a directory in \`${userToolsDir}//\` with two files: **TOOL.md** — Frontmatter metadata + markdown documentation: \`\`\`yaml --- name: my_tool description: What it does and when the agent should use it. version: 1 language: typescript inputs: param: type: string description: What this parameter is for. --- # My Tool Usage documentation here. \`\`\` **index.ts** — Must export an \`execute\` function: \`\`\`typescript type ToolResult = { content: Array<{ type: string; text: string }>; isError?: boolean }; export async function execute(_toolCallId: string, params: Record): Promise { return { content: [{ type: 'text', text: 'Done' }] }; } \`\`\` ## Environment Variables | Variable | Description | |----------|-------------| | \`OFFICER_EMAIL_DB\` | Path to email SQLite database | | \`PI_TOOLS_DIRS\` | Tool discovery paths (colon-separated) | | \`PI_SEARXNG_URL\` | Search engine URL | `; const contextDir = join(DATA_PATH, email, '.container-context'); mkdirSync(contextDir, { recursive: true }); const filePath = join(contextDir, 'CLAUDE.md'); writeFileSync(filePath, content); return filePath; } export function generateClaudeSettings(email: string, username?: string): string { const hostSettingsPath = join(process.env.HOME ?? '', '.claude', 'settings.json'); let settings: ClaudeSettings = {}; try { settings = JSON.parse(readFileSync(hostSettingsPath, 'utf-8')) as ClaudeSettings; } catch { // no host settings } const hookCommand = `curl -s -X POST http://localhost:5000/api/hooks/claude-done -H 'Content-Type: application/json' -d '{"email":"${email}"}'`; const hooks = settings.hooks ?? {}; const stopRules = hooks.Stop ?? []; const hasOurHook = stopRules.some((rule) => rule.hooks?.some((h) => h.command?.includes('/api/hooks/claude-done'))); if (!hasOurHook) { stopRules.push({ hooks: [{ type: 'command', command: hookCommand }] }); } hooks.Stop = stopRules; settings.hooks = hooks; settings.defaultMode = 'bypassPermissions'; settings.skipDangerousModePermissionPrompt = true; const contextDir = join(DATA_PATH, email, '.container-context'); mkdirSync(contextDir, { recursive: true }); const filePath = join(contextDir, 'settings.json'); writeFileSync(filePath, JSON.stringify(settings, null, 2)); // Pre-seed trust and skip-permissions in .claude.json so interactive Claude Code skips all prompts if (username) { const homeDir = getHomeDir(email); const claudeJsonPath = join(homeDir, '.claude.json'); let claudeJson: Record = {}; try { claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf-8')) as Record; } catch { // no existing config } const projects = (claudeJson.projects ?? {}) as Record>; const projectKey = homeDir; if (!projects[projectKey]) projects[projectKey] = {}; projects[projectKey]!.hasTrustDialogAccepted = true; claudeJson.projects = projects; writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2)); } return filePath; }