diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c92a0847ff..96d32e82de 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ jobs:
- name: Set up Node.js with npm cache
uses: actions/setup-node@v6
with:
- node-version: 22.12.0
+ node-version: '24'
cache: npm
- name: Install dependencies
@@ -43,5 +43,11 @@ jobs:
- name: Test
run: npm test
- - name: Build
+ - name: Build app
run: npm run build
+
+ - name: Build library
+ run: npm run build:lib
+
+ - name: Verify package contents
+ run: npm pack --dry-run
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
new file mode 100644
index 0000000000..5e8d29bbb7
--- /dev/null
+++ b/.github/workflows/npm-publish.yml
@@ -0,0 +1,94 @@
+name: Publish to npm
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+concurrency:
+ group: npm-publish-${{ github.ref }}
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+ id-token: write
+
+jobs:
+ publish:
+ name: Publish to npm
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ # Trusted publishing requires Node 22.14.0+ and npm 11.5.1+
+ # See: https://docs.npmjs.com/trusted-publishers/
+ - name: Set up Node.js for npm trusted publishing
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+ cache: npm
+ registry-url: https://registry.npmjs.org
+
+ - name: Verify npm version supports trusted publishing
+ run: |
+ echo "Node version: $(node --version)"
+ echo "npm version: $(npm --version)"
+ NPM_VERSION=$(npm --version)
+ NPM_MAJOR=$(echo $NPM_VERSION | cut -d. -f1)
+ NPM_MINOR=$(echo $NPM_VERSION | cut -d. -f2)
+ if [ "$NPM_MAJOR" -lt 11 ] || ([ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -lt 5 ]); then
+ echo "Error: npm 11.5.1+ required for trusted publishing, got $NPM_VERSION"
+ exit 1
+ fi
+ echo "✓ npm $NPM_VERSION meets trusted publishing requirements"
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build app
+ run: npm run build
+
+ - name: Build library
+ run: npm run build:lib
+
+ - name: Verify package contents
+ run: npm pack --dry-run
+
+ - name: Validate package version matches release tag
+ run: |
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
+ echo "Package version: $PACKAGE_VERSION"
+ echo "Release tag version: $TAG_VERSION"
+ if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
+ echo "Error: package.json version ($PACKAGE_VERSION) doesn't match release tag ($TAG_VERSION)"
+ exit 1
+ fi
+ echo "✓ Version $PACKAGE_VERSION matches release tag"
+
+ - name: Determine npm tag
+ id: npm-tag
+ run: |
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
+ if [[ "$TAG_VERSION" == *"alpha"* ]]; then
+ echo "tag=alpha" >> $GITHUB_OUTPUT
+ echo "Publishing with tag: alpha"
+ elif [[ "$TAG_VERSION" == *"beta"* ]]; then
+ echo "tag=beta" >> $GITHUB_OUTPUT
+ echo "Publishing with tag: beta"
+ elif [[ "$TAG_VERSION" == *"rc"* ]]; then
+ echo "tag=rc" >> $GITHUB_OUTPUT
+ echo "Publishing with tag: rc"
+ else
+ echo "tag=latest" >> $GITHUB_OUTPUT
+ echo "Publishing with tag: latest"
+ fi
+
+ - name: Publish to npm with provenance
+ run: npm publish --access public --provenance --tag ${{ steps.npm-tag.outputs.tag }}
diff --git a/.github/workflows/sdk-version-sync.yml b/.github/workflows/sdk-version-sync.yml
index 7441045d91..efe97d8fdf 100644
--- a/.github/workflows/sdk-version-sync.yml
+++ b/.github/workflows/sdk-version-sync.yml
@@ -55,7 +55,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
- node-version: 22.12.0
+ node-version: '24'
- name: Log trigger info
run: |
diff --git a/.gitignore b/.gitignore
index 945375afde..ef996a7db8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ src/i18n/declaration.ts
.envrc
node_modules/
build/
+dist/
/test-results/
@@ -14,3 +15,4 @@ build/
.react-router/
ralph/
.vercel
+*.tgz
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..8ab0df2d7f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,29 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [1.0.0-alpha.2] - 2025-05-11
+
+### Added
+
+- Initial npm package release of `@openhands/agent-canvas`
+- CLI entry point (`npx @openhands/agent-canvas`) to run full stack with Docker
+- Library build mode with component barrel exports
+- Subpath exports for modular imports:
+ - `@openhands/agent-canvas/browser`
+ - `@openhands/agent-canvas/conversation`
+ - `@openhands/agent-canvas/files`
+ - `@openhands/agent-canvas/settings`
+ - `@openhands/agent-canvas/sidebar`
+ - `@openhands/agent-canvas/terminal`
+ - `@openhands/agent-canvas/i18n`
+- TypeScript type declarations
+- GitHub Actions workflow for automated npm publishing (OIDC trusted publishing)
+
+[Unreleased]: https://github.com/OpenHands/agent-canvas/compare/v1.0.0-alpha.2...HEAD
+[1.0.0-alpha.2]: https://github.com/OpenHands/agent-canvas/releases/tag/v1.0.0-alpha.2
diff --git a/README.md b/README.md
index 9ee5224443..83b9c44c82 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,42 @@ The Agent Server is often paired with an [Automation Server](https://github.com/
+## npm Package
+
+Agent Canvas is also available as an npm package for embedding in your own applications:
+
+```bash
+npm install @openhands/agent-canvas
+```
+
+### Usage
+
+Import the full package or specific components:
+
+```typescript
+// Full package
+import { AgentServerUIProviders } from '@openhands/agent-canvas';
+
+// Individual component packages
+import { BrowserPanel } from '@openhands/agent-canvas/browser';
+import { ChatPanel } from '@openhands/agent-canvas/conversation';
+import { FileExplorer } from '@openhands/agent-canvas/files';
+import { TerminalPanel } from '@openhands/agent-canvas/terminal';
+```
+
+### Available Subpath Exports
+
+| Subpath | Description |
+|---------|-------------|
+| `@openhands/agent-canvas` | Main entry with providers and core components |
+| `@openhands/agent-canvas/browser` | Browser/preview panel components |
+| `@openhands/agent-canvas/conversation` | Chat interface and message components |
+| `@openhands/agent-canvas/files` | File explorer and editor components |
+| `@openhands/agent-canvas/settings` | Settings screens and forms |
+| `@openhands/agent-canvas/sidebar` | Sidebar navigation components |
+| `@openhands/agent-canvas/terminal` | Terminal emulator component |
+| `@openhands/agent-canvas/i18n` | Internationalization resources |
+
## More documentation
For contributor and developer workflows, including frontend-only mode, mock mode, environment variables, and build/test commands, see [DEVELOPMENT.md](./DEVELOPMENT.md).
diff --git a/bin/agent-canvas.mjs b/bin/agent-canvas.mjs
new file mode 100755
index 0000000000..44ff0f6d68
--- /dev/null
+++ b/bin/agent-canvas.mjs
@@ -0,0 +1,108 @@
+#!/usr/bin/env node
+/**
+ * CLI entry point for @openhands/agent-canvas
+ *
+ * Runs the full Agent Canvas stack using Docker for the agent-server:
+ * - Agent-server runs in Docker container
+ * - Automation backend via uvx
+ * - Pre-built static frontend (not Vite dev server)
+ *
+ * This is the production equivalent of `npm run dev` - it runs the full stack
+ * but serves pre-built static assets instead of the Vite dev server.
+ */
+
+import { existsSync } from "node:fs";
+import { join, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const BUILD_DIR = join(__dirname, "..", "build", "client");
+
+// Check for help flag first
+const args = process.argv.slice(2);
+if (args.includes("-h") || args.includes("--help")) {
+ console.log(`
+@openhands/agent-canvas - Run the Agent Canvas UI with agent-server (Docker)
+
+Runs the full stack with agent-server in Docker, automation backend via uvx,
+and serves pre-built static frontend assets.
+
+USAGE:
+ npx @openhands/agent-canvas [options]
+
+REQUIRED:
+ PROJECT_PATH Path to your projects directory (mounted into container)
+
+OPTIONS:
+ -p, --port Ingress port (default: 8000)
+ -h, --help Show this help message
+
+ENVIRONMENT VARIABLES:
+ PROJECT_PATH Required: path to your projects directory
+ OH_SECRET_KEY Secret key for encrypting settings
+ OH_AGENT_SERVER_GIT_REF Git ref for agent-server Docker image tag
+ OH_AGENT_SERVER_LOCAL_PATH Path to local SDK checkout (for development)
+ OH_MOUNT_HOST_HOME Set to "1" to mount entire home directory
+
+Note: LLM settings are configured through the web UI settings page,
+not environment variables.
+
+EXAMPLES:
+ # Start full stack (requires PROJECT_PATH)
+ PROJECT_PATH=/path/to/projects npx @openhands/agent-canvas
+
+ # Use a specific port
+ PROJECT_PATH=/path/to/projects npx @openhands/agent-canvas --port 3000
+
+ # Use local SDK checkout for development
+ PROJECT_PATH=/path/to/projects OH_AGENT_SERVER_LOCAL_PATH=/path/to/sdk npx @openhands/agent-canvas
+
+For more options, see: node scripts/dev-docker.mjs --help
+`);
+ process.exit(0);
+}
+
+// Check build exists before doing anything else
+if (!existsSync(BUILD_DIR)) {
+ console.error(`
+Error: No build found at ${BUILD_DIR}
+
+This package needs to be built first. If you installed from npm,
+this is a packaging error. If running from source:
+
+ npm install
+ npm run build
+`);
+ process.exit(1);
+}
+
+// Import dev-docker's dependencies and run with static mode
+let main, checkDockerPrereqs, startAgentServerDocker, CONTAINER_WORKSPACES_DIR;
+try {
+ ({ main } = await import("../scripts/dev-with-automation.mjs"));
+ ({
+ checkDockerPrereqs,
+ startAgentServerDocker,
+ CONTAINER_WORKSPACES_DIR,
+ } = await import("../scripts/dev-docker.mjs"));
+} catch (err) {
+ console.error("Failed to load required scripts. Try reinstalling:");
+ console.error(" npm install -g @openhands/agent-canvas@latest");
+ console.error(`\nError: ${err.message}`);
+ process.exit(1);
+}
+
+main({
+ bannerTitle: "Agent Canvas",
+ extraPrereqs: checkDockerPrereqs,
+ startAgentServer: startAgentServerDocker,
+ viteWorkingDir: CONTAINER_WORKSPACES_DIR,
+ staticMode: true,
+ staticDir: BUILD_DIR,
+}).catch((err) => {
+ console.error(`Fatal error: ${err.message}`);
+ if (err.stack) {
+ console.error(err.stack);
+ }
+ process.exit(1);
+});
diff --git a/package-lock.json b/package-lock.json
index ce711f1349..5a5939035a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@openhands/agent-canvas",
- "version": "1.6.0",
+ "version": "1.0.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openhands/agent-canvas",
- "version": "1.6.0",
+ "version": "1.0.0-alpha.2",
"license": "MIT",
"dependencies": {
"@heroui/react": "2.8.10",
@@ -53,6 +53,9 @@
"vite": "8.0.10",
"zustand": "5.0.12"
},
+ "bin": {
+ "agent-canvas": "bin/agent-canvas.mjs"
+ },
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.39.4",
diff --git a/package.json b/package.json
index 501a5910fc..00edbde559 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,21 @@
{
"name": "@openhands/agent-canvas",
- "version": "1.6.0",
+ "version": "1.0.0-alpha.2",
+ "description": "Agent Canvas UI for OpenHands - run AI coding agents with a visual interface",
"license": "MIT",
"private": false,
"type": "module",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/OpenHands/agent-canvas"
+ },
+ "homepage": "https://github.com/OpenHands/agent-canvas#readme",
+ "bugs": {
+ "url": "https://github.com/OpenHands/agent-canvas/issues"
+ },
+ "bin": {
+ "agent-canvas": "bin/agent-canvas.mjs"
+ },
"engines": {
"node": ">=22.12.0"
},
@@ -75,7 +87,7 @@
"prelint": "npm run make-i18n",
"lint": "npm run typecheck && eslint src && prettier --check src/**/*.{ts,tsx}",
"lint:fix": "eslint src --fix && prettier --write src/**/*.{ts,tsx}",
- "prepare": "cd .. && husky frontend/.husky",
+ "prepare": "[ -d '../.git' ] && cd .. && husky frontend/.husky || true",
"typecheck": "react-router typegen && tsc",
"typecheck:staged": "react-router typegen && npx tsc --noEmit --skipLibCheck",
"check-translation-completeness": "node scripts/check-translation-completeness.cjs",
@@ -154,7 +166,10 @@
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
- "dist"
+ "dist",
+ "bin",
+ "build",
+ "scripts"
],
"exports": {
".": {
diff --git a/scripts/dev-with-automation.mjs b/scripts/dev-with-automation.mjs
index 4a1cc3b8ba..af3f3bd0c4 100644
--- a/scripts/dev-with-automation.mjs
+++ b/scripts/dev-with-automation.mjs
@@ -38,7 +38,7 @@
*/
import { spawn, spawnSync } from "node:child_process";
-import { mkdirSync } from "node:fs";
+import { mkdirSync, existsSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { homedir } from "node:os";
@@ -116,6 +116,8 @@ function parseArgs() {
automationGitRef: null,
automationRepo: null,
verbose: false,
+ static: false,
+ staticDir: null,
};
for (let i = 0; i < args.length; i++) {
@@ -134,6 +136,12 @@ function parseArgs() {
case "--verbose":
config.verbose = true;
break;
+ case "--static":
+ config.static = true;
+ break;
+ case "--static-dir":
+ config.staticDir = args[++i];
+ break;
case "-h":
case "--help":
showHelp();
@@ -689,12 +697,21 @@ async function main(options = {}) {
startAgentServer: startAgentServerOverride,
extraPrereqs,
viteWorkingDir,
+ staticMode: staticModeOverride,
+ staticDir: staticDirOverride,
} = options;
const args = parseArgs();
+ // Allow options to override CLI args (for bin/agent-canvas.mjs)
+ const useStaticMode = staticModeOverride ?? args.static;
+ const staticDir = staticDirOverride ?? args.staticDir ?? join(projectRoot, "build", "client");
+
+ const modeLabel = useStaticMode ? "(Static)" : "";
+ const titleWithMode = modeLabel ? `${bannerTitle} ${modeLabel}` : bannerTitle;
+
console.log("");
- console.log(`${c.cyan}${c.bold}${bannerTitle}${c.reset}`);
+ console.log(`${c.cyan}${c.bold}${titleWithMode}${c.reset}`);
console.log("");
// Setup phase
@@ -702,6 +719,13 @@ async function main(options = {}) {
// backend runs via uvx; only the agent-server is dockerized.)
checkPrerequisites();
+ // In static mode, verify build exists
+ if (useStaticMode && !existsSync(staticDir)) {
+ logError(`Static directory not found: ${staticDir}`);
+ logError(`Run 'npm run build' first to create the static files.`);
+ process.exit(1);
+ }
+
// Build config with dynamic port allocation
const config = await buildConfig(args);
if (viteWorkingDir) config.viteWorkingDir = viteWorkingDir;
@@ -736,8 +760,12 @@ async function main(options = {}) {
// 3. Start automation backend
startAutomationBackend(config);
- // 4. Start Vite dev server (no proxy config needed - ingress handles routing)
- startVite(config);
+ // 4. Start frontend server (Vite dev server OR static server)
+ if (useStaticMode) {
+ startStaticFrontend(config, staticDir);
+ } else {
+ startVite(config);
+ }
// 5. Wait for services to be ready
await delay(2000);
@@ -751,6 +779,35 @@ async function main(options = {}) {
printBanner(config);
}
+function startStaticFrontend(config, staticDir) {
+ logService("static", `Starting on port ${config.vitePort}...`, c.magenta);
+ logService("static", `Serving from: ${staticDir}`, c.dim);
+
+ const staticServerScript = join(projectRoot, "scripts", "static-server.mjs");
+ spawnService(
+ "static",
+ "node",
+ [
+ staticServerScript,
+ "--dir", staticDir,
+ "--host", "0.0.0.0",
+ "--port", String(config.vitePort),
+ // Proxy routes to backends (same as ingress but for direct access to vitePort)
+ "--route", `/api/automation=http://localhost:${config.autoBackendPort}`,
+ "--route", `/api=http://localhost:${config.agentServerPort}`,
+ "--route", `/sockets=http://localhost:${config.agentServerPort}`,
+ "--route", `/server_info=http://localhost:${config.agentServerPort}`,
+ "--route", `/health=http://localhost:${config.agentServerPort}`,
+ "--route", `/ready=http://localhost:${config.agentServerPort}`,
+ "--route", `/alive=http://localhost:${config.agentServerPort}`,
+ ],
+ {
+ cwd: config.canvasPath,
+ color: c.magenta,
+ }
+ );
+}
+
// ═══════════════════════════════════════════════════════════════════════════
// Exports for testing
// ═══════════════════════════════════════════════════════════════════════════