#!/usr/bin/env node import { build } from 'esbuild'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const WORKER_SERVICE = { name: 'worker-service', source: 'src/services/worker-service.ts' }; const SERVER_SERVICE = { name: 'server-service', source: 'src/server/runtime/ServerService.ts' }; const MCP_SERVER = { name: 'mcp-server', source: 'src/servers/mcp-server.ts' }; const CONTEXT_GENERATOR = { name: 'context-generator', source: 'src/services/context-generator.ts' }; const TRANSCRIPT_WATCHER = { name: 'transcript-watcher', source: 'src/services/transcripts/transcript-watcher-entry.ts' }; function stripHardcodedDirname(filePath) { let content = fs.readFileSync(filePath, 'utf-8'); const before = content.length; const str = `(?:"[^"]*"|'[^']*')`; for (const id of ['__dirname', '__filename']) { content = content.replace(new RegExp(`\\bvar ${id}\\s*=\\s*${str},\\s*`, 'g'), 'var '); content = content.replace(new RegExp(`\\bvar ${id}\\s*=\\s*${str};\\s*`, 'g'), ''); content = content.replace(new RegExp(`,\\s*${id}\\s*=\\s*${str}`, 'g'), ''); } content = content.replace(/\bvar\s*;/g, ''); content = content.replace(/[ \t]+$/gm, ''); const removed = before - content.length; if (removed > 0) { fs.writeFileSync(filePath, content); console.log(` āœ“ Stripped hardcoded __dirname/__filename paths (${removed} bytes)`); } } /** * Rule A canonical-template manifest: maps each host-managed config file's * command string to the buildShellCommand() options that generate it. The * build asserts the hand-maintained files still match the generator output so * the defensive shell prelude can't drift between the three files (issues * #1215, #1533). See src/build/hook-shell-template.ts and CLAUDE.md → * "Spawn-Contract Resolution". */ function shellTemplateManifest(buildShellCommand, buildCodexWindowsCommand) { const ccTrailing = (...tail) => [ 'node', '"$_P/scripts/bun-runner.js"', '"$_P/scripts/worker-service.cjs"', ...tail, ]; const claudeHook = (tail, extra = {}) => buildShellCommand({ host: 'claude-code', requireFile: 'bun-runner.js', requireFileSecondary: 'worker-service.cjs', trailingCommand: ccTrailing(...tail), notFoundMessage: 'claude-mem: plugin scripts not found', ...extra, }); const codexHook = (tail) => buildShellCommand({ host: 'codex-cli', requireFile: 'bun-runner.js', requireFileSecondary: 'worker-service.cjs', trailingCommand: ccTrailing(...tail), notFoundMessage: 'claude-mem: plugin scripts not found', extraEnv: { CLAUDE_MEM_CODEX_HOOK: '1' }, }); const codexStartupHook = () => buildShellCommand({ host: 'codex-cli', requireFile: 'bun-runner.js', requireFileSecondary: 'worker-service.cjs', trailingCommand: [ '_V=$(CLAUDE_MEM_CODEX_HOOK=1 node "$_P/scripts/version-check.js" || true);', 'if [ -n "$_V" ]; then printf \'%s\\n\' "$_V"; else', 'CLAUDE_MEM_CODEX_HOOK=1', ...ccTrailing('hook', 'codex', 'context'), '; fi', ], notFoundMessage: 'claude-mem: plugin scripts not found', }); const codexHookPair = (tail, options = {}) => ({ command: options.startupVersionCheck ? codexStartupHook() : codexHook(tail), commandWindows: buildCodexWindowsCommand(tail, options), }); return { 'plugin/hooks/hooks.json': { kind: 'hooks', commands: { 'Setup.0.0': buildShellCommand({ host: 'claude-code-setup', requireFile: 'version-check.js', trailingCommand: ['node', '"$_P/scripts/version-check.js"'], notFoundMessage: 'claude-mem: version-check.js not found', }), 'SessionStart.0.0': claudeHook(['start'], { trailingJson: { continue: true, suppressOutput: true } }), 'SessionStart.0.1': claudeHook(['hook', 'claude-code', 'context']), 'UserPromptSubmit.0.0': claudeHook(['hook', 'claude-code', 'session-init']), 'PostToolUse.0.0': claudeHook(['hook', 'claude-code', 'observation']), 'PreToolUse.0.0': claudeHook(['hook', 'claude-code', 'file-context']), 'Stop.0.0': claudeHook(['hook', 'claude-code', 'summarize']), }, }, 'plugin/hooks/codex-hooks.json': { kind: 'hooks', commands: { 'SessionStart.0.0': codexHookPair(['hook', 'codex', 'context'], { startupVersionCheck: true }), 'UserPromptSubmit.0.0': codexHookPair(['hook', 'codex', 'session-init']), 'PreToolUse.0.0': codexHookPair(['hook', 'codex', 'file-context']), 'PostToolUse.0.0': codexHookPair(['hook', 'codex', 'observation']), 'Stop.0.0': codexHookPair(['hook', 'codex', 'summarize']), }, }, 'plugin/.mcp.json': { kind: 'mcp', command: buildShellCommand({ // The mcp Node launcher derives its spawn target from requireFile, so // no trailingCommand is needed (it is ignored for this host). host: 'mcp', requireFile: 'mcp-server.cjs', notFoundMessage: 'claude-mem: mcp server not found', mcpExtraCandidates: ['$PWD/plugin', '$PWD'], mcpExtraCacheRoots: [ '$HOME/.codex/plugins/cache/claude-mem-local/claude-mem', '$HOME/.codex/plugins/cache/thedotmack/claude-mem', ], }), }, }; } function hookEntryByPath(parsed, dottedPath) { const [event, groupIdx, hookIdx] = dottedPath.split('.'); return parsed.hooks?.[event]?.[Number(groupIdx)]?.hooks?.[Number(hookIdx)] ?? null; } async function verifyShellTemplateCanonical() { console.log('\nšŸ“‹ Verifying Rule A shell templates match the canonical generator...'); // Compile src/build/hook-shell-template.ts in-memory and import it. The build // runs under Node, which can't import .ts directly, so we bundle to ESM and // load via a data: URL. const bundled = await build({ entryPoints: ['src/build/hook-shell-template.ts'], bundle: true, platform: 'node', format: 'esm', write: false, logLevel: 'error', }); const moduleSource = bundled.outputFiles[0].text; const dataUrl = 'data:text/javascript;base64,' + Buffer.from(moduleSource).toString('base64'); const { buildShellCommand, buildCodexWindowsCommand } = await import(dataUrl); const manifest = shellTemplateManifest(buildShellCommand, buildCodexWindowsCommand); for (const [filePath, spec] of Object.entries(manifest)) { const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8')); if (spec.kind === 'mcp') { const actual = parsed.mcpServers?.['mcp-search']?.args?.[1] ?? ''; if (actual !== spec.command) { throw new Error( `Hand-edited shell string detected in ${filePath} (mcp-search). It no longer matches src/build/hook-shell-template.ts. ` + `Update the generator (and this manifest) instead of hand-editing the launcher.` ); } } else { for (const [dottedPath, expected] of Object.entries(spec.commands)) { const entry = hookEntryByPath(parsed, dottedPath); const expectedCommand = typeof expected === 'string' ? expected : expected.command; const actual = entry?.command ?? null; if (actual !== expectedCommand) { throw new Error( `Hand-edited shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` + `Regenerate via the canonical generator instead of hand-editing the command.` ); } if (typeof expected !== 'string') { const actualWindows = entry?.commandWindows ?? null; if (actualWindows !== expected.commandWindows) { throw new Error( `Hand-edited Windows shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` + `Regenerate via the canonical generator instead of hand-editing commandWindows.` ); } } } } } // Rule C safety net (bun-runner.js fixBrokenScriptPath) must stay documented. const bunRunner = fs.readFileSync('plugin/scripts/bun-runner.js', 'utf-8'); if (!bunRunner.includes('function fixBrokenScriptPath')) { throw new Error( 'plugin/scripts/bun-runner.js is missing fixBrokenScriptPath — it is the Rule C runtime safety net behind Rule A. Do not remove it.' ); } // Parser-compat guard (issue #2791): bun-runner.js is invoked by hosts that // may run a pre-ES2020 Node whose ESM loader throws on optional chaining. // Strip comments, then forbid `?.` / `??` in executable code. const bunRunnerCode = bunRunner .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/(^|[^:])\/\/.*$/gm, '$1'); if (/\?\.|\?\?/.test(bunRunnerCode)) { throw new Error( 'plugin/scripts/bun-runner.js uses optional chaining (?.) or nullish coalescing (??) — ' + 'this launcher must parse on pre-ES2020 Node (issue #2791). Rewrite with explicit guards.' ); } console.log('āœ“ Rule A shell templates match the canonical generator'); } async function buildHooks() { console.log('šŸ”Ø Building claude-mem hooks and worker service...\n'); try { const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')); const version = packageJson.version; console.log(`šŸ“Œ Version: ${version}`); console.log('\nšŸ“¦ Preparing output directories...'); const hooksDir = 'plugin/scripts'; const uiDir = 'plugin/ui'; if (!fs.existsSync(hooksDir)) { fs.mkdirSync(hooksDir, { recursive: true }); } if (!fs.existsSync(uiDir)) { fs.mkdirSync(uiDir, { recursive: true }); } console.log('āœ“ Output directories ready'); console.log('\nšŸ“¦ Generating plugin package.json...'); const pluginPackageJson = { name: 'claude-mem-plugin', version: version, private: true, description: 'Runtime dependencies for claude-mem bundled hooks', type: 'module', dependencies: { 'zod': '^4.4.3', 'tree-sitter-cli': '^0.26.5', 'tree-sitter-c': '^0.24.1', 'tree-sitter-cpp': '^0.23.4', 'tree-sitter-go': '^0.25.0', 'tree-sitter-java': '^0.23.5', 'tree-sitter-javascript': '^0.25.0', 'tree-sitter-python': '^0.25.0', 'tree-sitter-ruby': '^0.23.1', 'tree-sitter-rust': '^0.24.0', 'tree-sitter-typescript': '^0.23.2', 'tree-sitter-kotlin': '^0.3.8', 'tree-sitter-swift': '^0.7.1', 'tree-sitter-php': '^0.24.2', '@tree-sitter-grammars/tree-sitter-lua': '^0.4.1', 'tree-sitter-scala': '^0.24.0', 'tree-sitter-bash': '^0.25.1', 'tree-sitter-haskell': '^0.23.1', '@tree-sitter-grammars/tree-sitter-zig': '^1.1.2', 'tree-sitter-css': '^0.25.0', 'tree-sitter-scss': '^1.0.0', '@tree-sitter-grammars/tree-sitter-toml': '^0.7.0', '@tree-sitter-grammars/tree-sitter-yaml': '^0.7.1', '@derekstride/tree-sitter-sql': '^0.3.11', '@tree-sitter-grammars/tree-sitter-markdown': '^0.3.2', 'shell-quote': '^1.8.3', }, overrides: { 'tree-sitter': '^0.25.0' }, trustedDependencies: [ 'tree-sitter-cli' ], engines: { node: '>=20.12.0', bun: '>=1.0.0' } }; fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n'); console.log('āœ“ plugin/package.json generated'); console.log('\nšŸ“‹ Building React viewer...'); const { spawn } = await import('child_process'); const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' }); await new Promise((resolve, reject) => { viewerBuild.on('exit', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Viewer build failed with exit code ${code}`)); } }); }); console.log(`\nšŸ”§ Building worker service...`); await build({ entryPoints: [WORKER_SERVICE.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`, minify: true, logLevel: 'error', // Suppress warnings (import.meta warning is benign) external: [ 'bun:sqlite', 'zod', 'cohere-ai', 'ollama', '@chroma-core/default-embed', 'onnxruntime-node', // better-auth (~3.7MB) is only reachable through BetterAuthRoutes' request-time // dynamic import('better-auth/node') / import('./auth.js'). esbuild otherwise // inlines that dynamic-import target into the worker bundle, dragging in the full // better-auth library (kysely, oauth, nanoid, …) even though the worker never // exercises it (the dep isn't in the worker's runtime plugin/package.json deps, // and the route handler already wraps the import in try/catch → graceful 500). // Keeping it external strips the dead weight from worker-service.cjs. See #2584. 'better-auth', 'better-auth/node', 'better-auth/plugins', '@better-auth/api-key', ], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"`, // Polyfill import.meta.url for ESM deps bundled into CJS output. // @anthropic-ai/claude-agent-sdk's *.mjs files use createRequire(import.meta.url) // and `new URL(rel, import.meta.url)`. We map import.meta.url to a file:// URL // (not the raw __filename path) so URL construction preserves its semantics. 'import.meta.url': '__IMPORT_META_URL__' }, banner: { js: [ '#!/usr/bin/env bun', 'var __filename = __filename || require("node:path").resolve(process.argv[1] || "");', 'var __dirname = __dirname || require("node:path").dirname(__filename);', 'var __IMPORT_META_URL__ = require("node:url").pathToFileURL(__filename).href;' ].join('\n') } }); stripHardcodedDirname(`${hooksDir}/${WORKER_SERVICE.name}.cjs`); fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755); const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`); console.log(`āœ“ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`); // Advisory only — a sudden jump usually means a heavy server-only dependency // (better-auth, kysely, a database driver) leaked into the worker bundle via a // transitive import (#2584). Never blocks the build. const WORKER_SERVICE_MAX_BYTES = 2900 * 1024; if (workerStats.size > WORKER_SERVICE_MAX_BYTES) { console.warn( `āš ļø worker-service.cjs is ${(workerStats.size / 1024).toFixed(2)} KB (advisory budget ${(WORKER_SERVICE_MAX_BYTES / 1024).toFixed(0)} KB). ` + `If this jumped unexpectedly, check whether a server-only dependency leaked into the worker bundle (see #2584).` ); } // worker-service.cjs lazy-requires these via createRequire("../sqlite/…"), // intentionally kept external from the worker bundle (#2584). They must ship // as sibling files under plugin/sqlite/, or clean installs can throw // "Cannot find module '../sqlite/SessionStore.js'" when Chroma vector sync // reaches the SQLite helpers (#3107/#3126). console.log(`\nšŸ”§ Building sqlite runtime modules...`); const SQLITE_MODULES = [ { source: 'src/services/sqlite/SessionStore.ts', out: 'plugin/sqlite/SessionStore.js' }, { source: 'src/services/sqlite/observations/files.ts', out: 'plugin/sqlite/observations/files.js' }, ]; for (const mod of SQLITE_MODULES) { fs.mkdirSync(path.dirname(mod.out), { recursive: true }); await build({ entryPoints: [mod.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: mod.out, minify: true, logLevel: 'error', external: [ 'bun:sqlite', 'zod', 'cohere-ai', 'ollama', '@chroma-core/default-embed', 'onnxruntime-node', 'better-auth', 'better-auth/node', 'better-auth/plugins', '@better-auth/api-key', ], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"`, 'import.meta.url': '__IMPORT_META_URL__' }, banner: { js: 'var __IMPORT_META_URL__ = require("node:url").pathToFileURL(__filename).href;' } }); console.log(`āœ“ ${mod.out} built (${(fs.statSync(mod.out).size / 1024).toFixed(2)} KB)`); } console.log(`\nšŸ”§ Building server beta service...`); await build({ entryPoints: [SERVER_SERVICE.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${SERVER_SERVICE.name}.cjs`, minify: true, logLevel: 'error', external: [ 'bun:sqlite', 'zod', ], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: [ '#!/usr/bin/env bun', 'var __filename = __filename || require("node:path").resolve(process.argv[1] || "");', 'var __dirname = __dirname || require("node:path").dirname(__filename);' ].join('\n') } }); stripHardcodedDirname(`${hooksDir}/${SERVER_SERVICE.name}.cjs`); fs.chmodSync(`${hooksDir}/${SERVER_SERVICE.name}.cjs`, 0o755); const serverStats = fs.statSync(`${hooksDir}/${SERVER_SERVICE.name}.cjs`); console.log(`āœ“ server-service built (${(serverStats.size / 1024).toFixed(2)} KB)`); console.log(`\nšŸ”§ Building MCP server...`); await build({ entryPoints: [MCP_SERVER.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${MCP_SERVER.name}.cjs`, minify: true, logLevel: 'error', external: [ 'bun:sqlite', 'tree-sitter-cli', 'tree-sitter-javascript', 'tree-sitter-typescript', 'tree-sitter-python', 'tree-sitter-go', 'tree-sitter-rust', 'tree-sitter-ruby', 'tree-sitter-java', 'tree-sitter-c', 'tree-sitter-cpp', 'tree-sitter-kotlin', 'tree-sitter-swift', 'tree-sitter-php', '@tree-sitter-grammars/tree-sitter-lua', 'tree-sitter-scala', 'tree-sitter-bash', 'tree-sitter-haskell', '@tree-sitter-grammars/tree-sitter-zig', 'tree-sitter-css', 'tree-sitter-scss', '@tree-sitter-grammars/tree-sitter-toml', '@tree-sitter-grammars/tree-sitter-yaml', '@derekstride/tree-sitter-sql', '@tree-sitter-grammars/tree-sitter-markdown', ], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env node' } }); stripHardcodedDirname(`${hooksDir}/${MCP_SERVER.name}.cjs`); fs.chmodSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 0o755); const mcpServerStats = fs.statSync(`${hooksDir}/${MCP_SERVER.name}.cjs`); console.log(`āœ“ mcp-server built (${(mcpServerStats.size / 1024).toFixed(2)} KB)`); const mcpBundleContent = fs.readFileSync(`${hooksDir}/${MCP_SERVER.name}.cjs`, 'utf-8'); const bunRequireRegex = /require\(\s*["']bun:[a-z][a-z0-9_-]*["']\s*\)/; const bunRequireMatch = mcpBundleContent.match(bunRequireRegex); if (bunRequireMatch) { throw new Error( `mcp-server.cjs contains a Bun-only ${bunRequireMatch[0]} call. This means a transitive import in src/servers/mcp-server.ts pulled in code from worker-service.ts (or another module that touches DatabaseManager/ChromaSync). The MCP server runs under Node and cannot load bun:* modules. Audit recent imports in src/servers/mcp-server.ts and src/services/worker-spawner.ts — the spawner module is intentionally lightweight and MUST NOT import anything that touches SQLite or other Bun-only modules. See PR #1645 for context.` ); } const zodRequireRegex = /require\(\s*["']zod(?:\/[^"']*)?["']\s*\)/; const zodRequireMatch = mcpBundleContent.match(zodRequireRegex); if (zodRequireMatch) { throw new Error( `mcp-server.cjs contains external ${zodRequireMatch[0]}. Claude Desktop can launch this bundle without plugin node_modules available, so Zod must be bundled into the MCP server.` ); } const MCP_SERVER_MAX_BYTES = 600 * 1024; if (mcpServerStats.size > MCP_SERVER_MAX_BYTES) { console.warn( `āš ļø mcp-server.cjs is ${(mcpServerStats.size / 1024).toFixed(2)} KB (advisory budget ${(MCP_SERVER_MAX_BYTES / 1024).toFixed(0)} KB). If this jumped unexpectedly, a transitive import may have pulled worker-service.ts or another heavy module into the MCP bundle (see #1645).` ); } console.log(`\nšŸ”§ Building context generator...`); await build({ entryPoints: [CONTEXT_GENERATOR.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`, minify: true, logLevel: 'error', external: ['bun:sqlite', 'zod'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, // No banner needed: CJS files under Node.js have __dirname/__filename natively }); stripHardcodedDirname(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`); const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`); console.log(`āœ“ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`); console.log(`\nšŸ”§ Building transcript watcher...`); await build({ entryPoints: [TRANSCRIPT_WATCHER.source], bundle: true, platform: 'node', target: 'node18', format: 'cjs', outfile: `${hooksDir}/${TRANSCRIPT_WATCHER.name}.cjs`, minify: true, logLevel: 'error', // Externalize zod for consistency with worker-service / server-beta-service — // any zod usage in the processor.ts import chain should resolve at runtime // against plugin/node_modules instead of being inlined (avoids duplicate- // instance hazards and keeps the bundle slim). external: ['bun:sqlite', 'zod'], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, banner: { js: '#!/usr/bin/env bun' } }); stripHardcodedDirname(`${hooksDir}/${TRANSCRIPT_WATCHER.name}.cjs`); fs.chmodSync(`${hooksDir}/${TRANSCRIPT_WATCHER.name}.cjs`, 0o755); const transcriptWatcherStats = fs.statSync(`${hooksDir}/${TRANSCRIPT_WATCHER.name}.cjs`); console.log(`āœ“ transcript-watcher built (${(transcriptWatcherStats.size / 1024).toFixed(2)} KB)`); // Advisory only — the watcher is meant to be a thin file-tail loop. const TRANSCRIPT_WATCHER_MAX_BYTES = 200 * 1024; if (transcriptWatcherStats.size > TRANSCRIPT_WATCHER_MAX_BYTES) { console.warn( `āš ļø transcript-watcher.cjs is ${(transcriptWatcherStats.size / 1024).toFixed(2)} KB (advisory budget ${(TRANSCRIPT_WATCHER_MAX_BYTES / 1024).toFixed(0)} KB). If this jumped unexpectedly, check src/services/transcripts/processor.ts and watcher.ts for heavy imports.` ); } console.log(`\nšŸ”§ Building NPX CLI...`); const npxCliOutDir = 'dist/npx-cli'; if (!fs.existsSync(npxCliOutDir)) { fs.mkdirSync(npxCliOutDir, { recursive: true }); } await build({ entryPoints: ['src/npx-cli/index.ts'], bundle: true, platform: 'node', target: 'node18', format: 'esm', outfile: `${npxCliOutDir}/index.js`, banner: { js: '#!/usr/bin/env node' }, minify: true, logLevel: 'error', external: [ 'fs', 'fs/promises', 'path', 'os', 'child_process', 'url', 'crypto', 'http', 'https', 'net', 'stream', 'util', 'events', 'buffer', 'querystring', 'readline', 'tty', 'assert', 'bun:sqlite', ], define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }, }); fs.chmodSync(`${npxCliOutDir}/index.js`, 0o755); const npxCliStats = fs.statSync(`${npxCliOutDir}/index.js`); console.log(`āœ“ npx-cli built (${(npxCliStats.size / 1024).toFixed(2)} KB)`); if (fs.existsSync('openclaw/src/index.ts')) { console.log(`\nšŸ”§ Building OpenClaw plugin...`); const openclawOutDir = 'openclaw/dist'; if (!fs.existsSync(openclawOutDir)) { fs.mkdirSync(openclawOutDir, { recursive: true }); } await build({ entryPoints: ['openclaw/src/index.ts'], bundle: true, platform: 'node', target: 'node18', format: 'esm', outfile: `${openclawOutDir}/index.js`, minify: true, logLevel: 'error', external: [ 'fs', 'fs/promises', 'path', 'os', 'child_process', 'url', 'crypto', 'http', 'https', 'net', 'stream', 'util', 'events', ], }); const openclawStats = fs.statSync(`${openclawOutDir}/index.js`); console.log(`āœ“ openclaw plugin built (${(openclawStats.size / 1024).toFixed(2)} KB)`); } if (fs.existsSync('src/integrations/opencode-plugin/index.ts')) { console.log(`\nšŸ”§ Building OpenCode plugin...`); const opencodeOutDir = 'dist/opencode-plugin'; if (!fs.existsSync(opencodeOutDir)) { fs.mkdirSync(opencodeOutDir, { recursive: true }); } await build({ entryPoints: ['src/integrations/opencode-plugin/index.ts'], bundle: true, platform: 'node', target: 'node18', format: 'esm', outfile: `${opencodeOutDir}/index.js`, minify: true, logLevel: 'error', external: [ 'fs', 'fs/promises', 'path', 'os', 'child_process', 'url', 'crypto', 'http', 'https', 'net', 'stream', 'util', 'events', ], }); const opencodeStats = fs.statSync(`${opencodeOutDir}/index.js`); console.log(`āœ“ opencode plugin built (${(opencodeStats.size / 1024).toFixed(2)} KB)`); } console.log('\nšŸ“‹ Copying onboarding explainer to plugin tree...'); const onboardingExplainerSrc = 'src/services/worker/onboarding-explainer.md'; const onboardingExplainerDst = 'plugin/skills/how-it-works/onboarding-explainer.md'; if (!fs.existsSync(onboardingExplainerSrc)) { throw new Error(`Missing onboarding explainer source: ${onboardingExplainerSrc}`); } fs.mkdirSync(path.dirname(onboardingExplainerDst), { recursive: true }); fs.copyFileSync(onboardingExplainerSrc, onboardingExplainerDst); console.log(`āœ“ Copied ${onboardingExplainerSrc} → ${onboardingExplainerDst}`); console.log('\nšŸ“‹ Verifying distribution files...'); const validCodexHookEvents = new Set([ 'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'Stop', ]); const requiredDistributionFiles = [ 'plugin/skills/mem-search/SKILL.md', 'plugin/skills/smart-explore/SKILL.md', 'plugin/skills/how-it-works/SKILL.md', 'plugin/skills/how-it-works/onboarding-explainer.md', 'plugin/hooks/hooks.json', 'plugin/hooks/codex-hooks.json', 'plugin/scripts/bun-runner.js', 'plugin/sqlite/SessionStore.js', 'plugin/sqlite/observations/files.js', 'plugin/.claude-plugin/plugin.json', 'plugin/.codex-plugin/plugin.json', 'plugin/.mcp.json', '.codex-plugin/plugin.json', '.agents/plugins/marketplace.json', ]; for (const filePath of requiredDistributionFiles) { if (!fs.existsSync(filePath)) { throw new Error(`Missing required distribution file: ${filePath}`); } } const codexHooks = JSON.parse(fs.readFileSync('plugin/hooks/codex-hooks.json', 'utf-8')); const validCodexHookRootKeys = new Set(['hooks']); for (const rootKey of Object.keys(codexHooks)) { if (!validCodexHookRootKeys.has(rootKey)) { throw new Error(`plugin/hooks/codex-hooks.json contains unsupported Codex root key: ${rootKey}`); } } for (const eventName of Object.keys(codexHooks.hooks ?? {})) { if (!validCodexHookEvents.has(eventName)) { throw new Error(`plugin/hooks/codex-hooks.json contains unknown Codex hook event: ${eventName}`); } } const codexMarketplace = JSON.parse(fs.readFileSync('.agents/plugins/marketplace.json', 'utf-8')); const claudeMemMarketplaceEntry = (codexMarketplace.plugins ?? []).find((plugin) => plugin.name === 'claude-mem'); if (claudeMemMarketplaceEntry?.source?.path !== './plugin') { throw new Error('.agents/plugins/marketplace.json must point claude-mem source.path at ./plugin so Codex loads the bundled plugin root'); } const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8')); const mcpSearchCommand = bundledMcp.mcpServers?.['mcp-search']?.args?.join(' ') ?? ''; if (!mcpSearchCommand.includes('.codex/plugins/cache/claude-mem-local/claude-mem')) { throw new Error('plugin/.mcp.json mcp-search launcher must include Codex cache fallback for hosts that do not inject PLUGIN_ROOT'); } if (!mcpSearchCommand.includes('plugins/cache/thedotmack/claude-mem')) { throw new Error('plugin/.mcp.json mcp-search launcher must include Claude cache fallback for hosts that do not inject PLUGIN_ROOT'); } console.log('āœ“ All required distribution files present'); await verifyShellTemplateCanonical(); console.log('\nāœ… All build targets compiled successfully!'); console.log(` Output: ${hooksDir}/`); console.log(` - Worker: worker-service.cjs`); console.log(` - Server: server-service.cjs`); console.log(` - MCP Server: mcp-server.cjs`); console.log(` - Context Generator: context-generator.cjs`); console.log(` - Transcript Watcher: transcript-watcher.cjs`); console.log(` Output: ${npxCliOutDir}/`); console.log(` - NPX CLI: index.js`); if (fs.existsSync('openclaw/dist/index.js')) { console.log(` Output: openclaw/dist/`); console.log(` - OpenClaw Plugin: index.js`); } if (fs.existsSync('dist/opencode-plugin/index.js')) { console.log(` Output: dist/opencode-plugin/`); console.log(` - OpenCode Plugin: index.js`); } } catch (error) { console.error('\nāŒ Build failed:', error.message); if (error.errors) { console.error('\nBuild errors:'); error.errors.forEach(err => console.error(` - ${err.text}`)); } process.exit(1); } } buildHooks();