|
| 1 | +import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs'; |
| 2 | +import { join } from 'path'; |
| 3 | +import { homedir } from 'os'; |
| 4 | +import { logger } from './logger'; |
| 5 | +import { getResourcesDir } from './paths'; |
| 6 | + |
| 7 | +const CLAWX_BEGIN = '<!-- clawx:begin -->'; |
| 8 | +const CLAWX_END = '<!-- clawx:end -->'; |
| 9 | + |
| 10 | +/** |
| 11 | + * Merge a ClawX context section into an existing file's content. |
| 12 | + * If markers already exist, replaces the section in-place. |
| 13 | + * Otherwise appends it at the end. |
| 14 | + */ |
| 15 | +export function mergeClawXSection(existing: string, section: string): string { |
| 16 | + const wrapped = `${CLAWX_BEGIN}\n${section.trim()}\n${CLAWX_END}`; |
| 17 | + const beginIdx = existing.indexOf(CLAWX_BEGIN); |
| 18 | + const endIdx = existing.indexOf(CLAWX_END); |
| 19 | + if (beginIdx !== -1 && endIdx !== -1) { |
| 20 | + return existing.slice(0, beginIdx) + wrapped + existing.slice(endIdx + CLAWX_END.length); |
| 21 | + } |
| 22 | + return existing.trimEnd() + '\n\n' + wrapped + '\n'; |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Collect all unique workspace directories from the openclaw config: |
| 27 | + * the defaults workspace, each agent's workspace, and any workspace-* |
| 28 | + * directories that already exist under ~/.openclaw/. |
| 29 | + */ |
| 30 | +function resolveAllWorkspaceDirs(): string[] { |
| 31 | + const openclawDir = join(homedir(), '.openclaw'); |
| 32 | + const dirs = new Set<string>(); |
| 33 | + |
| 34 | + const configPath = join(openclawDir, 'openclaw.json'); |
| 35 | + try { |
| 36 | + if (existsSync(configPath)) { |
| 37 | + const config = JSON.parse(readFileSync(configPath, 'utf-8')); |
| 38 | + |
| 39 | + const defaultWs = config?.agents?.defaults?.workspace; |
| 40 | + if (typeof defaultWs === 'string' && defaultWs.trim()) { |
| 41 | + dirs.add(defaultWs.replace(/^~/, homedir())); |
| 42 | + } |
| 43 | + |
| 44 | + const agents = config?.agents?.list; |
| 45 | + if (Array.isArray(agents)) { |
| 46 | + for (const agent of agents) { |
| 47 | + const ws = agent?.workspace; |
| 48 | + if (typeof ws === 'string' && ws.trim()) { |
| 49 | + dirs.add(ws.replace(/^~/, homedir())); |
| 50 | + } |
| 51 | + } |
| 52 | + } |
| 53 | + } |
| 54 | + } catch { |
| 55 | + // ignore config parse errors |
| 56 | + } |
| 57 | + |
| 58 | + try { |
| 59 | + for (const entry of readdirSync(openclawDir, { withFileTypes: true })) { |
| 60 | + if (entry.isDirectory() && entry.name.startsWith('workspace')) { |
| 61 | + dirs.add(join(openclawDir, entry.name)); |
| 62 | + } |
| 63 | + } |
| 64 | + } catch { |
| 65 | + // ignore read errors |
| 66 | + } |
| 67 | + |
| 68 | + if (dirs.size === 0) { |
| 69 | + dirs.add(join(openclawDir, 'workspace')); |
| 70 | + } |
| 71 | + |
| 72 | + return [...dirs]; |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Ensure ClawX context snippets are merged into the openclaw workspace |
| 77 | + * bootstrap files. Reads `*.clawx.md` templates from resources/context/ |
| 78 | + * and injects them as marker-delimited sections into the corresponding |
| 79 | + * workspace `.md` files (e.g. AGENTS.clawx.md -> AGENTS.md). |
| 80 | + * |
| 81 | + * Iterates over every discovered agent workspace so all agents receive |
| 82 | + * the ClawX context regardless of which one is active. |
| 83 | + */ |
| 84 | +export function ensureClawXContext(): void { |
| 85 | + const contextDir = join(getResourcesDir(), 'context'); |
| 86 | + if (!existsSync(contextDir)) { |
| 87 | + logger.debug('ClawX context directory not found, skipping context merge'); |
| 88 | + return; |
| 89 | + } |
| 90 | + |
| 91 | + let files: string[]; |
| 92 | + try { |
| 93 | + files = readdirSync(contextDir).filter((f) => f.endsWith('.clawx.md')); |
| 94 | + } catch { |
| 95 | + return; |
| 96 | + } |
| 97 | + |
| 98 | + const workspaceDirs = resolveAllWorkspaceDirs(); |
| 99 | + |
| 100 | + for (const workspaceDir of workspaceDirs) { |
| 101 | + if (!existsSync(workspaceDir)) { |
| 102 | + mkdirSync(workspaceDir, { recursive: true }); |
| 103 | + } |
| 104 | + |
| 105 | + for (const file of files) { |
| 106 | + const targetName = file.replace('.clawx.md', '.md'); |
| 107 | + const targetPath = join(workspaceDir, targetName); |
| 108 | + const section = readFileSync(join(contextDir, file), 'utf-8'); |
| 109 | + |
| 110 | + let existing = ''; |
| 111 | + if (existsSync(targetPath)) { |
| 112 | + existing = readFileSync(targetPath, 'utf-8'); |
| 113 | + } |
| 114 | + |
| 115 | + const merged = mergeClawXSection(existing, section); |
| 116 | + if (merged !== existing) { |
| 117 | + writeFileSync(targetPath, merged, 'utf-8'); |
| 118 | + logger.info(`Merged ClawX context into ${targetName} (${workspaceDir})`); |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | +} |
0 commit comments