/** * OpenClaude build script — bundles the TypeScript source into a single * distributable JS file using Bun's bundler. * * Handles: * - bun:bundle feature() flags for the open build * - MACRO.* globals → inlined version/build-time constants * - src/ path aliases */ import { readFileSync, readdirSync, writeFileSync } from 'fs' import { join } from 'path' import { noTelemetryPlugin } from './no-telemetry-plugin' const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) const version = pkg.version // Feature flags for the open build. // Most Anthropic-internal features stay off; open-build features can be // selectively enabled here when their full source exists in the mirror. const featureFlags: Record = { // ── Disabled: require Anthropic infrastructure or missing source ───── VOICE_MODE: false, // Push-to-talk STT via claude.ai OAuth endpoint PROACTIVE: false, // Autonomous agent mode (missing proactive/ module) KAIROS: false, // Persistent assistant/session mode (cloud backend) BRIDGE_MODE: false, // Remote desktop bridge via CCR infrastructure DAEMON: false, // Background daemon process (stubbed in open build) AGENT_TRIGGERS: false, // Scheduled remote agent triggers ABLATION_BASELINE: false, // A/B testing harness for eval experiments CONTEXT_COLLAPSE: false, // Context collapsing optimization (stubbed) COMMIT_ATTRIBUTION: false, // Co-Authored-By metadata in git commits UDS_INBOX: false, // Unix Domain Socket inter-session messaging BG_SESSIONS: false, // Background sessions via tmux (stubbed) WEB_BROWSER_TOOL: false, // Built-in browser automation (source not mirrored) CHICAGO_MCP: false, // Computer-use MCP (native Swift modules stubbed) COWORKER_TYPE_TELEMETRY: false, // Telemetry for agent/coworker type classification MCP_SKILLS: false, // Dynamic MCP skill discovery (src/skills/mcpSkills.ts not mirrored; enabling this causes "fetchMcpSkillsForClient is not a function" when MCP servers with resources connect — see #856) // ── Enabled: upstream defaults ────────────────────────────────────── COORDINATOR_MODE: true, // Multi-agent coordinator with worker delegation BUILTIN_EXPLORE_PLAN_AGENTS: true, // Built-in Explore/Plan specialized subagents BUDDY: true, // Buddy mode for paired programming MONITOR_TOOL: true, // MCP server monitoring/streaming tool TEAMMEM: true, // Team memory management MESSAGE_ACTIONS: true, // Message action buttons in the UI // ── Enabled: new activations ──────────────────────────────────────── DUMP_SYSTEM_PROMPT: true, // --dump-system-prompt CLI flag for debugging CACHED_MICROCOMPACT: true, // Cache-aware tool result truncation optimization AWAY_SUMMARY: true, // "While you were away" recap after 5min blur TRANSCRIPT_CLASSIFIER: true, // Auto-approval classifier for safe tool uses ULTRATHINK: true, // Deep thinking mode — type "ultrathink" to boost reasoning TOKEN_BUDGET: true, // Token budget tracking with usage warnings HISTORY_PICKER: true, // Enhanced interactive prompt history picker QUICK_SEARCH: true, // Ctrl+G quick search across prompts SHOT_STATS: true, // Shot distribution stats in session summary EXTRACT_MEMORIES: true, // Auto-extract durable memories from conversations FORK_SUBAGENT: true, // Implicit context-forking when omitting subagent_type VERIFICATION_AGENT: true, // Built-in read-only agent for test/verification PROMPT_CACHE_BREAK_DETECTION: true, // Detect & log unexpected prompt cache invalidations HOOK_PROMPTS: true, // Allow tools to request interactive user prompts } // ── Pre-process: replace feature() calls with boolean literals ────── // Bun v1.3.9+ resolves `import { feature } from 'bun:bundle'` natively // before plugins can intercept it via onResolve. The bun: namespace is // handled by Bun's C++ resolver which runs before the JS plugin phase, // so the previous onResolve/onLoad shim was silently ineffective — ALL // feature() calls evaluated to false regardless of the featureFlags map. // // Fix: pre-process source files to strip the bun:bundle import and // replace feature('FLAG') calls with their boolean literal. Files are // modified in-place before Bun.build() and restored in a finally block. // Match feature('FLAG') calls, including multi-line: feature(\n 'FLAG',\n) const featureCallRe = /\bfeature\(\s*['"](\w+)['"][,\s]*\)/gs const featureImportRe = /import\s*\{[^}]*\bfeature\b[^}]*\}\s*from\s*['"]bun:bundle['"];?\s*\n?/g const modifiedFiles = new Map() // path → original content function preProcessFeatureFlags(dir: string) { for (const ent of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, ent.name) if (ent.isDirectory()) { preProcessFeatureFlags(full); continue } if (!/\.(ts|tsx)$/.test(ent.name)) continue const raw = readFileSync(full, 'utf-8') if (!raw.includes('feature(')) continue let contents = raw contents = contents.replace(featureImportRe, '') contents = contents.replace(featureCallRe, (_match, name) => String((featureFlags as Record)[name] ?? false), ) if (contents !== raw) { modifiedFiles.set(full, raw) writeFileSync(full, contents) } } } function restoreModifiedFiles() { for (const [path, original] of modifiedFiles) { writeFileSync(path, original) } modifiedFiles.clear() } preProcessFeatureFlags(join(import.meta.dir, '..', 'src')) const numModified = modifiedFiles.size // Restore source files on abrupt termination (Ctrl+C, kill, etc.) for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { restoreModifiedFiles() process.exit(signal === 'SIGINT' ? 130 : 143) }) } try { const result = await Bun.build({ entrypoints: ['./src/entrypoints/cli.tsx'], outdir: './dist', target: 'node', format: 'esm', splitting: false, sourcemap: 'external', minify: false, naming: 'cli.mjs', define: { // MACRO.* build-time constants // Keep the internal compatibility version high enough to pass // first-party minimum-version guards, but expose the real package // version separately in Open Claude branding. 'MACRO.VERSION': JSON.stringify('99.0.0'), 'MACRO.DISPLAY_VERSION': JSON.stringify(version), 'MACRO.BUILD_TIME': JSON.stringify(new Date().toISOString()), 'MACRO.ISSUES_EXPLAINER': JSON.stringify('report the issue at https://github.com/anthropics/claude-code/issues'), 'MACRO.PACKAGE_URL': JSON.stringify('@gitlawb/openclaude'), 'MACRO.NATIVE_PACKAGE_URL': 'undefined', }, plugins: [ noTelemetryPlugin, { name: 'bun-bundle-shim', setup(build) { const internalFeatureStubModules = new Map([ [ '../daemon/workerRegistry.js', 'export async function runDaemonWorker() { throw new Error("Daemon worker is unavailable in the open build."); }', ], [ '../daemon/main.js', 'export async function daemonMain() { throw new Error("Daemon mode is unavailable in the open build."); }', ], [ '../cli/bg.js', ` export async function psHandler() { throw new Error("Background sessions are unavailable in the open build."); } export async function logsHandler() { throw new Error("Background sessions are unavailable in the open build."); } export async function attachHandler() { throw new Error("Background sessions are unavailable in the open build."); } export async function killHandler() { throw new Error("Background sessions are unavailable in the open build."); } export async function handleBgFlag() { throw new Error("Background sessions are unavailable in the open build."); } `, ], [ '../cli/handlers/templateJobs.js', 'export async function templatesMain() { throw new Error("Template jobs are unavailable in the open build."); }', ], [ '../environment-runner/main.js', 'export async function environmentRunnerMain() { throw new Error("Environment runner is unavailable in the open build."); }', ], [ '../self-hosted-runner/main.js', 'export async function selfHostedRunnerMain() { throw new Error("Self-hosted runner is unavailable in the open build."); }', ], ] as const) // bun:bundle feature() replacement is handled by the source // pre-processing step above (see preProcessFeatureFlags). // The previous onResolve/onLoad shim was ineffective in Bun // v1.3.9+ because the bun: namespace is resolved natively // before the JS plugin phase runs. build.onResolve( { filter: /^\.\.\/(daemon\/workerRegistry|daemon\/main|cli\/bg|cli\/handlers\/templateJobs|environment-runner\/main|self-hosted-runner\/main)\.js$/ }, args => { if (!internalFeatureStubModules.has(args.path)) return null return { path: args.path, namespace: 'internal-feature-stub', } }, ) build.onLoad( { filter: /.*/, namespace: 'internal-feature-stub' }, args => ({ contents: internalFeatureStubModules.get(args.path) ?? 'export {}', loader: 'js', }), ) // Resolve react/compiler-runtime to the standalone package build.onResolve({ filter: /^react\/compiler-runtime$/ }, () => ({ path: 'react/compiler-runtime', namespace: 'react-compiler-shim', })) build.onLoad( { filter: /.*/, namespace: 'react-compiler-shim' }, () => ({ contents: `export function c(size) { return new Array(size).fill(Symbol.for('react.memo_cache_sentinel')); }`, loader: 'js', }), ) // NOTE: @opentelemetry/* kept as external deps (too many named exports to stub) // Resolve native addon and missing snapshot imports to stubs for (const mod of [ 'audio-capture-napi', 'audio-capture.node', 'image-processor-napi', 'modifiers-napi', 'url-handler-napi', 'color-diff-napi', '@anthropic-ai/mcpb', '@ant/claude-for-chrome-mcp', '@anthropic-ai/sandbox-runtime', 'asciichart', 'plist', 'cacache', 'fuse', 'code-excerpt', 'stack-utils', ]) { build.onResolve({ filter: new RegExp(`^${mod}$`) }, () => ({ path: mod, namespace: 'native-stub', })) } build.onLoad( { filter: /.*/, namespace: 'native-stub' }, () => ({ // Comprehensive stub that handles any named export via Proxy contents: ` const noop = () => null; const noopClass = class {}; const handler = { get(_, prop) { if (prop === '__esModule') return true; if (prop === 'default') return new Proxy({}, handler); if (prop === 'ExportResultCode') return { SUCCESS: 0, FAILED: 1 }; if (prop === 'resourceFromAttributes') return () => ({}); if (prop === 'SandboxRuntimeConfigSchema') return { parse: () => ({}) }; return noop; } }; const stub = new Proxy(noop, handler); export default stub; export const __stub = true; // Named exports for all known imports export const SandboxViolationStore = null; export const SandboxManager = new Proxy({}, { get: () => noop }); export const SandboxRuntimeConfigSchema = { parse: () => ({}) }; export const BROWSER_TOOLS = []; export const getMcpConfigForManifest = noop; export const ColorDiff = null; export const ColorFile = null; export const getSyntaxTheme = noop; export const plot = noop; export const createClaudeForChromeMcpServer = noop; // OpenTelemetry exports export const ExportResultCode = { SUCCESS: 0, FAILED: 1 }; export const resourceFromAttributes = noop; export const Resource = noopClass; export const SimpleSpanProcessor = noopClass; export const BatchSpanProcessor = noopClass; export const NodeTracerProvider = noopClass; export const BasicTracerProvider = noopClass; export const OTLPTraceExporter = noopClass; export const OTLPLogExporter = noopClass; export const OTLPMetricExporter = noopClass; export const PrometheusExporter = noopClass; export const LoggerProvider = noopClass; export const SimpleLogRecordProcessor = noopClass; export const BatchLogRecordProcessor = noopClass; export const MeterProvider = noopClass; export const PeriodicExportingMetricReader = noopClass; export const trace = { getTracer: () => ({ startSpan: () => ({ end: noop, setAttribute: noop, setStatus: noop, recordException: noop }) }) }; export const context = { active: noop, with: (_, fn) => fn() }; export const SpanStatusCode = { OK: 0, ERROR: 1, UNSET: 2 }; export const ATTR_SERVICE_NAME = 'service.name'; export const ATTR_SERVICE_VERSION = 'service.version'; export const SEMRESATTRS_SERVICE_NAME = 'service.name'; export const SEMRESATTRS_SERVICE_VERSION = 'service.version'; export const AggregationTemporality = { CUMULATIVE: 0, DELTA: 1 }; export const DataPointType = { HISTOGRAM: 0, SUM: 1, GAUGE: 2 }; export const InstrumentType = { COUNTER: 0, HISTOGRAM: 1, UP_DOWN_COUNTER: 2 }; export const PushMetricExporter = noopClass; export const SeverityNumber = {}; `, loader: 'js', }), ) // Resolve .md and .txt file imports to empty string stubs build.onResolve({ filter: /\.(md|txt)$/ }, (args) => ({ path: args.path, namespace: 'text-stub', })) build.onLoad( { filter: /.*/, namespace: 'text-stub' }, () => ({ contents: `export default '';`, loader: 'js', }), ) // Pre-scan: find all missing modules that need stubbing // (Bun's onResolve corrupts module graph even when returning null, // so we use exact-match resolvers instead of catch-all patterns) const fs = require('fs') const pathMod = require('path') const srcDir = pathMod.resolve(__dirname, '..', 'src') const missingModules = new Set() const missingModuleExports = new Map>() // Known missing external packages for (const pkg of [ '@ant/computer-use-mcp', '@ant/computer-use-mcp/sentinelApps', '@ant/computer-use-mcp/types', '@ant/computer-use-swift', '@ant/computer-use-input', ]) { missingModules.add(pkg) } // Scan source to find imports that can't resolve function scanForMissingImports() { function checkAndRegister(specifier: string, fileDir: string, namedPart: string) { const names = namedPart.split(',') .map((s: string) => s.trim().replace(/^type\s+/, '')) .filter((s: string) => s && !s.startsWith('type ')) // Check src/tasks/ non-relative imports if (specifier.startsWith('src/tasks/')) { const resolved = pathMod.resolve(__dirname, '..', specifier) const candidates = [ resolved, `${resolved}.ts`, `${resolved}.tsx`, resolved.replace(/\.js$/, '.ts'), resolved.replace(/\.js$/, '.tsx'), pathMod.join(resolved, 'index.ts'), pathMod.join(resolved, 'index.tsx'), ] if (!candidates.some((c: string) => fs.existsSync(c))) { missingModules.add(specifier) } } // Check relative .js imports else if (specifier.endsWith('.js') && (specifier.startsWith('./') || specifier.startsWith('../'))) { const resolved = pathMod.resolve(fileDir, specifier) const tsVariant = resolved.replace(/\.js$/, '.ts') const tsxVariant = resolved.replace(/\.js$/, '.tsx') if (!fs.existsSync(resolved) && !fs.existsSync(tsVariant) && !fs.existsSync(tsxVariant)) { missingModules.add(specifier) } } // Track named exports for missing modules if (names.length > 0) { if (!missingModuleExports.has(specifier)) missingModuleExports.set(specifier, new Set()) for (const n of names) missingModuleExports.get(specifier)!.add(n) } } function walk(dir: string) { for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { const full = pathMod.join(dir, ent.name) if (ent.isDirectory()) { walk(full); continue } if (!/\.(ts|tsx)$/.test(ent.name)) continue const rawCode: string = fs.readFileSync(full, 'utf-8') const fileDir = pathMod.dirname(full) // Strip comments before scanning for imports/requires. // The regex scanner matches require()/import() patterns // inside JSDoc comments, causing false-positive missing // module detection that breaks the build with noop stubs. const code = rawCode .replace(/\/\*[\s\S]*?\*\//g, '') // block comments .replace(/\/\/.*$/gm, '') // line comments // Collect static imports: import { X } from '...' for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) { checkAndRegister(m[4], fileDir, m[1] || m[3] || '') } // Collect dynamic requires: require('...') — these are used // behind feature() gates and become live when flags are enabled. for (const m of code.matchAll(/require\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) { checkAndRegister(m[1], fileDir, '') } // Collect dynamic imports: import('...') for (const m of code.matchAll(/import\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g)) { checkAndRegister(m[1], fileDir, '') } } } walk(srcDir) } scanForMissingImports() // Register exact-match resolvers for each missing module for (const mod of missingModules) { const escaped = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({ path: mod, namespace: 'missing-module-stub', })) } build.onLoad( { filter: /.*/, namespace: 'missing-module-stub' }, (args) => { const names = missingModuleExports.get(args.path) ?? new Set() const exports = [...names].map(n => `export const ${n} = noop;`).join('\n') return { contents: ` const noop = () => null; export default noop; ${exports} `, loader: 'js', } }, ) }, }, ], external: [ // OpenTelemetry — too many named exports to stub, kept external '@opentelemetry/api', '@opentelemetry/api-logs', '@opentelemetry/core', '@opentelemetry/exporter-trace-otlp-grpc', '@opentelemetry/exporter-trace-otlp-http', '@opentelemetry/exporter-trace-otlp-proto', '@opentelemetry/exporter-logs-otlp-http', '@opentelemetry/exporter-logs-otlp-proto', '@opentelemetry/exporter-logs-otlp-grpc', '@opentelemetry/exporter-metrics-otlp-proto', '@opentelemetry/exporter-metrics-otlp-grpc', '@opentelemetry/exporter-metrics-otlp-http', '@opentelemetry/exporter-prometheus', '@opentelemetry/resources', '@opentelemetry/sdk-trace-base', '@opentelemetry/sdk-trace-node', '@opentelemetry/sdk-logs', '@opentelemetry/sdk-metrics', '@opentelemetry/semantic-conventions', // Native image processing 'sharp', // Cloud provider SDKs '@aws-sdk/client-bedrock', '@aws-sdk/client-bedrock-runtime', '@aws-sdk/client-sts', '@aws-sdk/credential-providers', '@azure/identity', 'google-auth-library', // @vscode/ripgrep ships a platform-specific binary alongside its // index.js and resolves the path via __dirname at runtime. Bundling // would freeze the build host's absolute path into dist/cli.mjs, so we // keep it external and rely on the npm package being installed. '@vscode/ripgrep', ], }) if (!result.success) { console.error('Build failed:') for (const log of result.logs) { console.error(log) } process.exitCode = 1 } else { console.log(`✓ Built openclaude v${version} → dist/cli.mjs`) } } finally { // Always restore source files, even if Bun.build() throws restoreModifiedFiles() console.log(` 🔄 feature-flags: pre-processed ${numModified} files (restored)`) }