feat: add build system, stubs, and npm packaging — openclaude is now runnable

- package.json with all 70+ dependencies
- Bun build script with feature flag shims, native module stubs, otel externals
- Stubs for ~15 missing source files (snapshot gaps)
- tsconfig.json for TypeScript
- bin/openclaude entry point
- Builds to single 19MB dist/cli.mjs
- Verified: --version and --help work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
did:key:z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr
2026-04-01 02:36:07 +08:00
parent fd108243eb
commit 3e652cafdf
30 changed files with 1988 additions and 2 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
dist/
*.tsbuildinfo
.env
.env.*

View File

@@ -8,6 +8,42 @@ All of Claude Code's tools work — bash, file read/write/edit, grep, glob, agen
---
## Install
### Option A: npm (recommended)
```bash
npm install -g openclaude
```
### Option B: From source (requires Bun)
```bash
# Clone from gitlawb
git clone https://node.gitlawb.com/z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr/openclaude.git
cd openclaude
# Install dependencies
bun install
# Build
bun run build
# Link globally (optional)
npm link
```
### Option C: Run directly with Bun (no build step)
```bash
git clone https://node.gitlawb.com/z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr/openclaude.git
cd openclaude
bun install
bun run dev
```
---
## Quick Start
### 1. Set 3 environment variables
@@ -18,10 +54,16 @@ export OPENAI_API_KEY=sk-your-key-here
export OPENAI_MODEL=gpt-4o
```
### 2. Run Claude Code
### 2. Run it
```bash
claude
# If installed via npm
openclaude
# If built from source
bun run dev
# or after build:
node dist/cli.mjs
```
That's it. The tool system, streaming, file editing, multi-step reasoning — everything works through the model you picked.

32
bin/openclaude Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* OpenClaude — Claude Code with any LLM
*
* If dist/cli.mjs exists (built), run that.
* Otherwise, tell the user to build first or use `bun run dev`.
*/
import { existsSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const distPath = join(__dirname, '..', 'dist', 'cli.mjs')
if (existsSync(distPath)) {
await import(distPath)
} else {
console.error(`
openclaude: dist/cli.mjs not found.
Build first:
bun run build
Or run directly with Bun:
bun run dev
See README.md for setup instructions.
`)
process.exit(1)
}

1435
bun.lock Normal file

File diff suppressed because it is too large Load Diff

105
package.json Normal file
View File

@@ -0,0 +1,105 @@
{
"name": "openclaude",
"version": "0.1.0",
"description": "Claude Code opened to any LLM — OpenAI, Gemini, DeepSeek, Ollama, and 200+ models",
"type": "module",
"bin": {
"openclaude": "./bin/openclaude"
},
"scripts": {
"build": "bun run scripts/build.ts",
"dev": "bun run src/entrypoints/cli.tsx",
"start": "node dist/cli.mjs"
},
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0",
"@anthropic-ai/bedrock-sdk": "^0.26.0",
"@anthropic-ai/foundry-sdk": "^0.2.0",
"@anthropic-ai/sdk": "^0.81.0",
"@anthropic-ai/vertex-sdk": "^0.14.0",
"@commander-js/extra-typings": "^12.0.0",
"@growthbook/growthbook": "^1.3.0",
"@modelcontextprotocol/sdk": "^1.12.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.214.0",
"@opentelemetry/core": "^2.6.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/sdk-logs": "^0.214.0",
"@opentelemetry/sdk-metrics": "^2.6.1",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/sdk-trace-node": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"ajv": "^8.17.0",
"axios": "^1.14.0",
"bidi-js": "^1.0.3",
"chalk": "^5.4.0",
"chokidar": "^4.0.0",
"cli-boxes": "^3.0.0",
"cli-highlight": "^2.1.0",
"commander": "^12.0.0",
"diff": "^7.0.0",
"emoji-regex": "^10.4.0",
"env-paths": "^3.0.0",
"execa": "^9.5.0",
"fflate": "^0.8.2",
"figures": "^6.1.0",
"fuse.js": "^7.1.0",
"get-east-asian-width": "^1.3.0",
"google-auth-library": "^9.15.0",
"https-proxy-agent": "^7.0.6",
"ignore": "^7.0.0",
"indent-string": "^5.0.0",
"jsonc-parser": "^3.3.1",
"lodash-es": "^4.17.21",
"lru-cache": "^11.0.0",
"marked": "^15.0.0",
"p-map": "^7.0.3",
"picomatch": "^4.0.0",
"proper-lockfile": "^4.1.2",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.29.2",
"semver": "^7.6.3",
"shell-quote": "^1.8.2",
"signal-exit": "^4.1.0",
"strip-ansi": "^7.1.0",
"supports-hyperlinks": "^3.1.0",
"tree-kill": "^1.2.2",
"turndown": "^7.2.0",
"type-fest": "^4.30.0",
"undici": "^7.3.0",
"usehooks-ts": "^3.1.1",
"vscode-languageserver-protocol": "^3.17.5",
"wrap-ansi": "^9.0.0",
"ws": "^8.18.0",
"xss": "^1.0.15",
"yaml": "^2.7.0",
"zod": "^3.24.0"
},
"devDependencies": {
"@types/bun": "^1.2.0",
"@types/react": "^18.3.0",
"typescript": "^5.7.0"
},
"engines": {
"node": ">=20.0.0"
},
"repository": {
"type": "git",
"url": "https://gitlawb.com/z6MkqDnb7Siv3Cwj7pGJq4T5EsUisECqR8KpnDLwcaZq5TPr/openclaude"
},
"keywords": [
"claude-code",
"openai",
"llm",
"cli",
"agent",
"deepseek",
"ollama",
"gemini"
],
"license": "MIT"
}

236
scripts/build.ts Normal file
View File

@@ -0,0 +1,236 @@
/**
* OpenClaude build script — bundles the TypeScript source into a single
* distributable JS file using Bun's bundler.
*
* Handles:
* - bun:bundle feature() flags → all false (disables internal-only features)
* - MACRO.* globals → inlined version/build-time constants
* - src/ path aliases
*/
import { readFileSync } from 'fs'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const version = pkg.version
// Feature flags — all disabled for the open build.
// These gate Anthropic-internal features (voice, proactive, kairos, etc.)
const featureFlags: Record<string, boolean> = {
VOICE_MODE: false,
PROACTIVE: false,
KAIROS: false,
BRIDGE_MODE: false,
DAEMON: false,
AGENT_TRIGGERS: false,
MONITOR_TOOL: false,
ABLATION_BASELINE: false,
DUMP_SYSTEM_PROMPT: false,
CACHED_MICROCOMPACT: false,
COORDINATOR_MODE: false,
CONTEXT_COLLAPSE: false,
COMMIT_ATTRIBUTION: false,
TEAMMEM: false,
UDS_INBOX: false,
BG_SESSIONS: false,
AWAY_SUMMARY: false,
TRANSCRIPT_CLASSIFIER: false,
WEB_BROWSER_TOOL: false,
MESSAGE_ACTIONS: false,
BUDDY: false,
CHICAGO_MCP: false,
COWORKER_TYPE_TELEMETRY: false,
}
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
'MACRO.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'),
},
plugins: [
{
name: 'bun-bundle-shim',
setup(build) {
// Resolve `import { feature } from 'bun:bundle'` to a shim
build.onResolve({ filter: /^bun:bundle$/ }, () => ({
path: 'bun:bundle',
namespace: 'bun-bundle-shim',
}))
build.onLoad(
{ filter: /.*/, namespace: 'bun-bundle-shim' },
() => ({
contents: `export function feature(name) { return false; }`,
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',
'sharp',
'@anthropic-ai/mcpb',
'@ant/claude-for-chrome-mcp',
'@anthropic-ai/sandbox-runtime',
'asciichart',
'plist',
'cacache',
'fuse',
'auto-bind',
'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',
}),
)
},
},
],
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',
// 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',
],
})
if (!result.success) {
console.error('Build failed:')
for (const log of result.logs) {
console.error(log)
}
process.exit(1)
}
console.log(`✓ Built openclaude v${version} → dist/cli.mjs`)

View File

@@ -0,0 +1,10 @@
// Stub — AssistantSessionChooser not included in source snapshot
import React from 'react'
export function AssistantSessionChooser(_props: {
sessions: unknown[]
onSelect: (id: string) => void
onCancel: () => void
}) {
return null
}

View File

@@ -0,0 +1,2 @@
// Stub
export default null

View File

@@ -0,0 +1,2 @@
// Stub
export default null

View File

@@ -0,0 +1,2 @@
// Stub — assistant command not included in source snapshot
export default null

View File

@@ -0,0 +1,3 @@
// Stub
import React from 'react'
export function SnapshotUpdateDialog(_props: unknown) { return null }

View File

@@ -0,0 +1,2 @@
// Stub — generated types not included in source snapshot
export type {}

View File

@@ -0,0 +1,2 @@
// Stub — SDK runtime types not included in source snapshot
export type {}

View File

@@ -0,0 +1,2 @@
// Stub — SDK tool types not included in source snapshot
export type {}

2
src/ink/devtools.ts Normal file
View File

@@ -0,0 +1,2 @@
// Stub — devtools not included in source snapshot
export default {}

9
src/ink/global.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
// Stub — global types for Ink renderer
declare namespace JSX {
interface IntrinsicElements {
'ink-box': Record<string, unknown>
'ink-text': Record<string, unknown>
'ink-root': Record<string, unknown>
'ink-virtual-text': Record<string, unknown>
}
}

View File

@@ -0,0 +1,12 @@
// Stub — cachedMicrocompact not included in source snapshot (feature-gated)
export function isCachedMicrocompactEnabled(): boolean {
return false
}
export function isModelSupportedForCacheEditing(_model: string): boolean {
return false
}
export function getCachedMCConfig() {
return null
}

View File

@@ -0,0 +1,4 @@
// Stub — snipCompact not included in source snapshot
export function snipCompact() {
return null
}

View File

@@ -0,0 +1,7 @@
// Stub — contextCollapse not included in source snapshot (feature-gated)
export function isContextCollapseEnabled(): boolean {
return false
}
export function getContextCollapseState() {
return null
}

View File

@@ -0,0 +1,3 @@
// Stub
export default null
export const REPLTool = null

View File

@@ -0,0 +1,3 @@
// Stub
export default null
export const SuggestBackgroundPRTool = null

View File

@@ -0,0 +1,2 @@
// Stub — TungstenTool not included in source snapshot (internal tool)
export const TungstenLiveMonitor = null

View File

@@ -0,0 +1,2 @@
// Stub
export const TungstenTool = null

View File

@@ -0,0 +1,3 @@
// Stub
export default null
export const VerifyPlanExecutionTool = null

View File

@@ -0,0 +1,2 @@
// Stub — WorkflowTool not included in source snapshot
export const WORKFLOW_TOOL_NAME = 'WorkflowTool'

View File

@@ -0,0 +1,18 @@
// Stub — original type not included in source snapshot
export interface ConnectorTextBlock {
type: 'connector_text'
text: string
}
export interface ConnectorTextDelta {
type: 'connector_text_delta'
text: string
}
export function isConnectorTextBlock(block: unknown): block is ConnectorTextBlock {
return (
typeof block === 'object' &&
block !== null &&
(block as Record<string, unknown>).type === 'connector_text'
)
}

View File

@@ -0,0 +1,13 @@
// Stub — types not included in source snapshot
export const OUTPUTS_SUBDIR = 'tool-results'
export interface PersistedFile {
path: string
content: string
size: number
}
export type TurnStartTime = number
export const DEFAULT_UPLOAD_CONCURRENCY = 5
export const FILE_COUNT_LIMIT = 100

View File

@@ -0,0 +1,3 @@
// Stub
export function isProtectedNamespace(_ns: string): boolean { return false }
export const PROTECTED_NAMESPACES: string[] = []

View File

@@ -0,0 +1 @@
This is a planning prompt stub.

22
tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}