rippanteq7 commited on
Commit
a35e79e
·
1 Parent(s): fdba223
.github/workflows/docker-build.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker build
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ docker-build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Docker Buildx
17
+ uses: docker/setup-buildx-action@v3
18
+
19
+ # Build only (no push). If this fails, the PR breaks container builds.
20
+ - name: Build Docker image
21
+ uses: docker/build-push-action@v6
22
+ with:
23
+ context: .
24
+ file: ./Dockerfile
25
+ push: false
26
+ tags: clawdbot-railway-template:ci
27
+ cache-from: type=gha
28
+ cache-to: type=gha,mode=max
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node
2
+ node_modules
3
+ npm-debug.log*
4
+
5
+ # Local env
6
+ .env
7
+
8
+ # Build artifacts
9
+ /dist
10
+
11
+ # Backup exports
12
+ *.tar.gz
13
+
14
+ # OS
15
+ .DS_Store
.planning/codebase/ARCHITECTURE.md ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture
2
+
3
+ **Analysis Date:** 2026-01-29
4
+
5
+ ## Pattern Overview
6
+
7
+ **Overall:** Wrapper Architecture with Reverse Proxy
8
+
9
+ **Key Characteristics:**
10
+ - Node.js-based wrapper around the Clawdbot CLI
11
+ - Express.js web server with HTTP proxy functionality
12
+ - Separate internal gateway (port 18789) and public wrapper (port 8080)
13
+ - Configuration management through JSON files and environment variables
14
+ - Built-in setup wizard for initial onboarding
15
+
16
+ ## Layers
17
+
18
+ **Wrapper Layer (Public Interface):**
19
+ - Purpose: Provides web interface and proxy to internal gateway
20
+ - Location: `[src/server.js]`
21
+ - Contains: Express app, HTTP proxy setup, configuration management
22
+ - Depends on: Node.js built-ins, express, http-proxy, tar
23
+ - Used by: End users via web browser, Railway deployment
24
+
25
+ **Gateway Layer (Internal Processing):**
26
+ - Purpose: Runs the actual Clawbot instance with CLI commands
27
+ - Location: Spawned child process at `[src/server.js]` lines 117-124
28
+ - Contains: Child process management, token authentication
29
+ - Depends on: Clawdbot CLI binary (built from source)
30
+ - Used by: Wrapper proxy via internal HTTP endpoint
31
+
32
+ **Configuration Layer:**
33
+ - Purpose: Manages application state and configuration persistence
34
+ - Location: `[src/server.js]` lines 16-44, 62-64, 66-72
35
+ - Contains: Path resolution, config file management, token persistence
36
+ - Depends on: Node.js file system operations
37
+ - Used by: All layers for configuration state
38
+
39
+ **Setup Layer (Onboarding):**
40
+ - Purpose: Web-based configuration wizard
41
+ - Location: `[src/setup-app.js]` and endpoints in `[src/server.js]` lines 200-548
42
+ - Contains: UI HTML, client-side JavaScript, setup API endpoints
43
+ - Depends on: Express web server, child process execution
44
+ - Used by: First-time users for initial setup
45
+
46
+ ## Data Flow
47
+
48
+ **Setup Flow:**
49
+ 1. User visits `/setup` → HTML form served
50
+ 2. Form submits to `/setup/api/run` → executes `clawdbot onboard`
51
+ 3. Configuration file written to state directory
52
+ 4. Gateway started as child process on loopback:18789
53
+ 5. User redirected to `/clawdbot` → proxied to gateway
54
+
55
+ **Runtime Flow:**
56
+ 1. Request to any endpoint (except `/setup`) → check if configured
57
+ 2. If not configured → redirect to `/setup`
58
+ 3. If configured → ensure gateway running
59
+ 4. Proxy request to internal gateway at `http://127.0.0.1:18789`
60
+
61
+ **Gateway Management:**
62
+ 1. Gateway spawned with token authentication
63
+ 2. Health check loop monitors gateway availability
64
+ 3. On failure → restart gateway process
65
+ 4. Token persisted for stable authentication
66
+
67
+ ## Key Abstractions
68
+
69
+ **Gateway Abstraction:**
70
+ - Purpose: Isolate external access from internal Clawdbot execution
71
+ - Examples: `[src/server.js]` lines 49-52, 97-153
72
+ - Pattern: Process management with health monitoring
73
+
74
+ **Configuration Abstraction:**
75
+ - Purpose: Centralized state management with environment fallbacks
76
+ - Examples: `[src/server.js]` lines 24-46, 62-64
77
+ - Pattern: Resolver functions with file persistence
78
+
79
+ **Authentication Abstraction:**
80
+ - Purpose: Layered security with setup password and gateway token
81
+ - Examples: `[src/server.js]` lines 19-21, 169-191, 24-46
82
+ - Pattern: Basic auth for setup, token auth for gateway
83
+
84
+ ## Entry Points
85
+
86
+ **Primary Entry:**
87
+ - Location: `[src/server.js]` line 193
88
+ - Triggers: Express server start on port 8080
89
+ - Responsibilities: Initialize app, setup proxy, handle graceful shutdown
90
+
91
+ **Setup Entry:**
92
+ - Location: `[src/server.js]` line 200
93
+ - Triggers: GET `/setup` requests
94
+ - Responsibilities: Serve setup wizard UI, handle configuration
95
+
96
+ **Health Check:**
97
+ - Location: `[src/server.js]` line 198
98
+ - Triggers: GET `/setup/healthz` requests
99
+ - Responsibilities: Respond with service health status
100
+
101
+ ## Error Handling
102
+
103
+ **Strategy:** Graceful degradation with descriptive messages
104
+
105
+ **Patterns:**
106
+ - Gateway startup failures → return 503 with error message
107
+ - Configuration missing → redirect to setup
108
+ - Proxy errors → log and continue serving
109
+ - Process spawn errors → capture and return in output
110
+
111
+ ## Cross-Cutting Concerns
112
+
113
+ **Logging:** Console logging with prefixes `[wrapper]`, `[gateway]`, `[proxy]`
114
+ **Validation:** Environment variable parsing, file system error handling
115
+ **Authentication:** Basic auth for setup endpoints, token auth for gateway
116
+ **State Management:** File-based persistence with environment overrides
117
+
118
+ ---
119
+
120
+ *Architecture analysis: 2026-01-29*
.planning/codebase/CONCERNS.md ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Codebase Concerns
2
+
3
+ **Analysis Date:** 2026-01-29
4
+
5
+ ## Tech Debt
6
+
7
+ **Hardcoded Magic Numbers and Strings:**
8
+ - Issue: PORT, INTERNAL_GATEWAY_PORT, timeouts hardcoded without configuration
9
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 15, 50, 82)
10
+ - Impact: Makes deployment difficult to customize and tune
11
+ - Fix approach: Move all magic numbers to environment variables with sensible defaults
12
+
13
+ **Basic Auth Security Shortcut:**
14
+ - Issue: SETUP_PASSWORD uses Basic Auth which sends credentials in plaintext
15
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 169-191)
16
+ - Impact: credentials exposed in logs and network traffic
17
+ - Fix approach: Use proper session-based authentication or HTTPS
18
+
19
+ **Lack of Input Validation:**
20
+ - Issue: No validation on auth tokens and config parameters
21
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 476, 498, 523)
22
+ - Impact: Potential injection attacks and malformed configuration
23
+ - Fix approach: Add validation for all input parameters before processing
24
+
25
+ **State Directory Management:**
26
+ - Issue: Multiple hardcoded state directory operations without proper error handling
27
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 38, 39, 101, 102, 586)
28
+ - Impact: Silent failures could leave system in inconsistent state
29
+ - Fix approach: Add proper error handling and atomic operations
30
+
31
+ ## Known Bugs
32
+
33
+ **Race Condition in Gateway Start:**
34
+ - Issue: Multiple concurrent requests could start multiple gateway instances
35
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 140-152)
36
+ - Symptoms: Duplicate processes, resource exhaustion
37
+ - Workaround: Use file-based locking or process singleton pattern
38
+
39
+ **Proxy Error Handling:**
40
+ - Issue: Proxy errors only logged to console, no graceful degradation
41
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 650-652, 694-695)
42
+ - Symptoms: WebSocket connections may drop without proper error handling
43
+ - Workaround: Add circuit breaker pattern and retry logic
44
+
45
+ **Token File Race Condition:**
46
+ - Issue: Gateway token file created without atomic operation
47
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 36-44)
48
+ - Symptoms: Could create inconsistent tokens in high-concurrency scenarios
49
+ - Workaround: Use file locking or atomic write operations
50
+
51
+ ## Security Considerations
52
+
53
+ **Basic Auth Exposure:**
54
+ - Risk: SETUP_PASSWORD transmitted in Base64 encoding (easily reversible)
55
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 177-189)
56
+ - Current mitigation: Only works over HTTPS in production
57
+ - Recommendations: Use proper session tokens or OAuth2
58
+
59
+ **Token Storage on Shared Systems:**
60
+ - Risk: Gateway token stored in user home directory with 600 permissions
61
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (line 39)
62
+ - Current mitigation: File mode 0600 limits access
63
+ - Recommendations: Use encrypted storage or secret management service
64
+
65
+ **Path Traversal in Export:**
66
+ - Risk: Export function could expose sensitive files via path manipulation
67
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 605-621)
68
+ - Current mitigation: Path sanitization logic
69
+ - Recommendations: Validate all paths and use chroot-style isolation
70
+
71
+ ## Performance Bottlenecks
72
+
73
+ **Synchronous File Operations:**
74
+ - Issue: Multiple sync file operations during setup
75
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 29, 30, 68, 586)
76
+ - Problem: Blocks event loop during configuration checks
77
+ - Improvement path: Use async/await for all file operations
78
+
79
+ **No Request Timeout on Proxy:**
80
+ - Issue: Proxy requests have no timeout configuration
81
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 668)
82
+ - Problem: Long-running requests could hang the proxy
83
+ - Improvement path: Add configurable timeouts per endpoint
84
+
85
+ **Memory Leaks in Child Processes:**
86
+ - Issue: Child processes not properly cleaned up on exit
87
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 697-705)
88
+ - Problem: SIGTERM handler doesn't wait for graceful shutdown
89
+ - Improvement path: Implement proper process cleanup and signal handling
90
+
91
+ ## Fragile Areas
92
+
93
+ **CLI Dependency Management:**
94
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 297, 566)
95
+ - Why fragile: Assumes clawdbot CLI exists and is executable
96
+ - Safe modification: Add fallback handling for missing CLI
97
+ - Test coverage: No tests for CLI failure scenarios
98
+
99
+ **Channel Feature Detection:**
100
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 469, 472, 495)
101
+ - Why fragile: Relies on CLI help text parsing for feature detection
102
+ - Safe modification: Use API responses instead of help text parsing
103
+ - Test coverage: No tests for different clawdbot builds
104
+
105
+ **WebSocket Proxy Implementation:**
106
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js` (lines 683-695)
107
+ - Why fragile: Custom WebSocket proxy implementation may not handle all edge cases
108
+ - Safe modification: Use established WebSocket proxy library
109
+ - Test coverage: No WebSocket-specific tests
110
+
111
+ ## Scaling Limits
112
+
113
+ **Single Gateway Instance:**
114
+ - Current capacity: Single process handling all requests
115
+ - Limit: Memory and CPU bottlenecks under high load
116
+ - Scaling path: Implement gateway sharding or load balancing
117
+
118
+ **File-Based State Management:**
119
+ - Current capacity: Single directory for all state
120
+ - Limit: File system contention with concurrent access
121
+ - Scaling path: Use database or distributed state storage
122
+
123
+ **Hardcoded Port Binding:**
124
+ - Current capacity: Single port 8080
125
+ - Limit: Cannot scale horizontally without port conflicts
126
+ - Scaling path: Support port range configuration and service discovery
127
+
128
+ ## Dependencies at Risk
129
+
130
+ **express@^5.1.0:**
131
+ - Risk: Major version 5 is alpha/beta with breaking changes
132
+ - Impact: Entire server could break with npm updates
133
+ - Migration plan: Pin to specific stable version or use framework-agnostic HTTP server
134
+
135
+ **http-proxy@^1.18.1:**
136
+ - Risk: Older package with potential security vulnerabilities
137
+ - Impact: Proxy functionality could be compromised
138
+ - Migration plan: Use modern proxy library with active maintenance
139
+
140
+ ## Missing Critical Features
141
+
142
+ **No Health Monitoring:**
143
+ - Problem: Limited health endpoints beyond basic check
144
+ - Blocks: Proper observability and alerting
145
+ - Recommendation: Add comprehensive health checks with metrics
146
+
147
+ **No Rate Limiting:**
148
+ - Problem: No protection against abuse of setup endpoints
149
+ - Blocks: Service stability under attack
150
+ - Recommendation: Implement rate limiting for authentication endpoints
151
+
152
+ **No Audit Logging:**
153
+ - Problem: No record of configuration changes or access
154
+ - Blocks: Security incident investigation
155
+ - Recommendation: Add audit logging for all administrative actions
156
+
157
+ ## Test Coverage Gaps
158
+
159
+ **No Unit Tests:**
160
+ - What's not tested: Server logic, proxy functionality, authentication
161
+ - Files: `C:\Users\derek\clawdbot-railway-template\src\server.js`
162
+ - Risk: Undiscovered bugs in core functionality
163
+ - Priority: High - critical service components untested
164
+
165
+ **No Integration Tests:**
166
+ - What's not tested: Gateway startup process, configuration flow
167
+ - Files: Core server flow from lines 97-167
168
+ - Risk: Integration failures between components
169
+ - Priority: Medium - important but not critical
170
+
171
+ **No Error Scenario Tests:**
172
+ - What's not tested: CLI failures, file permission errors, network issues
173
+ - Files: Error handling throughout server.js
174
+ - Risk: Graceful degradation not verified
175
+ - Priority: Medium - affects reliability
176
+
177
+ ---
178
+
179
+ *Concerns audit: 2026-01-29*
.planning/codebase/CONVENTIONS.md ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Coding Conventions
2
+
3
+ **Analysis Date:** 2024-01-29
4
+
5
+ ## Naming Patterns
6
+
7
+ **Files:**
8
+ - kebab-case for server files: `src/server.js`
9
+ - kebab-case for utility scripts: `scripts/smoke.js`
10
+ - descriptive names reflecting purpose
11
+
12
+ **Functions:**
13
+ - camelCase for function names: `startGateway()`, `ensureGatewayRunning()`, `resolveGatewayToken()`
14
+ - predicate functions: `isConfigured()`
15
+ - async functions: `waitForGatewayReady()`, `buildOnboardArgs()`
16
+
17
+ **Variables:**
18
+ - UPPER_SNAKE_CASE for constants: `PORT`, `STATE_DIR`, `WORKSPACE_DIR`, `INTERNAL_GATEWAY_PORT`
19
+ - camelCase for regular variables: `gatewayProc`, `gatewayStarting`, `setupPayload`
20
+ - prefix with `_` for private variables (limited usage)
21
+
22
+ **Types:**
23
+ - Not applicable - no TypeScript in codebase
24
+
25
+ ## Code Style
26
+
27
+ **Formatting:**
28
+ - Standard JavaScript formatting (no linter config found)
29
+ - 2-space indentation
30
+ - Line length varies (pragmatic approach for long configurations)
31
+ - Semicolons used consistently
32
+ - Mixed quotes (single and double)
33
+
34
+ **Linting:**
35
+ - Minimal linting: `node -c src/server.js` (basic syntax check only)
36
+ - No ESLint/Prettier configuration
37
+ - No automated code style enforcement
38
+
39
+ ## Import Organization
40
+
41
+ **Order:**
42
+ 1. Node.js core imports (first group)
43
+ 2. Third-party imports (second group)
44
+ 3. Relative imports (third group - not used)
45
+
46
+ **Path Aliases:**
47
+ - Not used
48
+ - Relative imports not needed due to flat structure
49
+
50
+ ## Error Handling
51
+
52
+ **Patterns:**
53
+ - try/catch blocks with specific error handling
54
+ - Error propagation with meaningful messages
55
+ - Logging errors to console with prefixes: `[gateway]`, `[proxy]`, `[/setup/api/run]`
56
+ - Graceful degradation (ignore errors in non-critical paths)
57
+
58
+ ```javascript
59
+ try {
60
+ fs.writeFileSync(tokenPath, generated, { encoding: "utf8", mode: 0o600 });
61
+ } catch {
62
+ // best-effort
63
+ }
64
+ ```
65
+
66
+ ## Logging
67
+
68
+ **Framework:** console.log
69
+
70
+ **Patterns:**
71
+ - Bracketed prefixes for log sources: `[wrapper]`, `[gateway]`, `[proxy]`
72
+ - Status messages during startup
73
+ - Error logging with context
74
+ - No structured logging framework
75
+
76
+ ## Comments
77
+
78
+ **When to Comment:**
79
+ - Complex configuration logic (auth groups, channel setup)
80
+ - Gateway management explanations
81
+ - Proxy setup details
82
+ - Environment variable explanations
83
+
84
+ **JSDoc/TSDoc:**
85
+ - Not used
86
+ - Function documentation limited to inline comments
87
+
88
+ ## Function Design
89
+
90
+ **Size:**
91
+ - Mix of small (helper functions) and large (server.js handlers) functions
92
+ - `server.js` contains many nested functions and complex handlers
93
+
94
+ **Parameters:**
95
+ - Consistent use of default parameters: `opts = {}`
96
+ - Optional chaining: `req.body?.{}`
97
+ - Nullish coalescing: `process.env.CLAWDBOT_PUBLIC_PORT ?? process.env.PORT ?? "8080"`
98
+
99
+ **Return Values:**
100
+ - Object returns for status: `{ ok: true }`, `{ ok: false, reason: "..." }`
101
+ - Promise-based async returns
102
+ - Direct returns for simple cases
103
+
104
+ ## Module Design
105
+
106
+ **Exports:**
107
+ - No explicit exports (single application entry points)
108
+ - IIFE pattern for setup-app.js: `(function () { ... })();`
109
+
110
+ **Barrel Files:**
111
+ - Not used
112
+
113
+ ---
114
+
115
+ *Convention analysis: 2024-01-29*
116
+ *Note: Limited formal conventions - pragmatic Node.js approach*
.planning/codebase/INTEGRATIONS.md ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # External Integrations
2
+
3
+ **Analysis Date:** 2026-01-29
4
+
5
+ ## APIs & External Services
6
+
7
+ **AI Provider Groups:**
8
+ - **OpenAI** - Codex OAuth + API key
9
+ - Options: Codex CLI, ChatGPT OAuth, API key
10
+ - **Anthropic** - Claude Code CLI + API key
11
+ - Options: Claude Code token, setup-token, API key
12
+ - **Google** - Gemini API key + OAuth
13
+ - Options: Gemini API key, Antigravity OAuth, Gemini CLI OAuth
14
+ - **OpenRouter** - API key
15
+ - **Vercel AI Gateway** - API key
16
+ - **Moonshot AI** - Kimi K2 + Kimi Code
17
+ - Options: API key, Kimi Code API key
18
+ - **Z.AI (GLM 4.7)** - API key
19
+ - **MiniMax** - M2.1 (recommended)
20
+ - Options: M2.1, M2.1 Lightning
21
+ - **Qwen** - OAuth
22
+ - Options: Qwen OAuth
23
+ - **Copilot** - GitHub + local proxy
24
+ - Options: GitHub Copilot (device login), Copilot Proxy
25
+ - **Synthetic** - Anthropic-compatible (multi-model)
26
+ - **OpenCode Zen** - API key
27
+
28
+ **Messaging Platforms:**
29
+ - **Telegram** - Bot tokens for channel integration
30
+ - **Discord** - Bot tokens with message content intent
31
+ - **Slack** - Bot tokens and app tokens
32
+
33
+ ## Data Storage
34
+
35
+ **Databases:**
36
+ - None detected (clawdbot likely handles its own data)
37
+
38
+ **File Storage:**
39
+ - Local filesystem for state and workspace directories
40
+ - Export functionality creates .tar.gz archives
41
+
42
+ **Caching:**
43
+ - Not detected in codebase
44
+
45
+ ## Authentication & Identity
46
+
47
+ **Auth Provider:**
48
+ - Custom token-based authentication for gateway
49
+ - Basic auth for setup endpoint
50
+ - Support for multiple AI provider authentication methods
51
+
52
+ **Implementation:**
53
+ - Gateway tokens persisted to filesystem
54
+ - Setup password protection via HTTP Basic Auth
55
+ - Environment variable configuration
56
+
57
+ ## Monitoring & Observability
58
+
59
+ **Error Tracking:**
60
+ - Console logging for errors
61
+ - Custom error handling in proxy and gateway processes
62
+
63
+ **Logs:**
64
+ - Console output from wrapper and clawdbot processes
65
+ - Error logging for proxy and gateway issues
66
+ - Debug endpoints for troubleshooting
67
+
68
+ ## CI/CD & Deployment
69
+
70
+ **Hosting:**
71
+ - Railway platform deployment
72
+ - Docker containerization
73
+
74
+ **CI Pipeline:**
75
+ - GitHub Actions for Docker builds
76
+ - Automated builds on PR and push to main
77
+
78
+ **Health Checks:**
79
+ - Railway healthcheck endpoint at /setup/healthz
80
+ - Gateway readiness monitoring
81
+
82
+ ## Environment Configuration
83
+
84
+ **Required env vars:**
85
+ - `PORT` - HTTP server port (8080 default)
86
+ - `CLAWDBOT_PUBLIC_PORT` - Override port for Railway
87
+ - `SETUP_PASSWORD` - Password for setup endpoint
88
+ - `CLAWDBOT_GATEWAY_TOKEN` - Authentication token
89
+ - `CLAWDBOT_STATE_DIR` - State directory path
90
+ - `CLAWDBOT_WORKSPACE_DIR` - Workspace directory path
91
+ - `CLAWDBOT_ENTRY` - Path to clawdbot executable
92
+ - `CLAWDBOT_NODE` - Node.js executable path
93
+
94
+ **Secrets location:**
95
+ - Environment variables
96
+ - Token persistence in state directory (gateway.token)
97
+
98
+ ## Webhooks & Callbacks
99
+
100
+ **Incoming:**
101
+ - `/setup` - Configuration wizard
102
+ - `/setup/api/*` - Setup API endpoints
103
+ - `/setup/healthz` - Health check
104
+
105
+ **Outgoing:**
106
+ - Proxy requests to internal gateway
107
+ - External AI provider API calls
108
+ - Messaging platform API calls
109
+
110
+ ---
111
+
112
+ *Integration audit: 2026-01-29*
113
+ ```
.planning/codebase/STACK.md ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Technology Stack
2
+
3
+ **Analysis Date:** 2026-01-29
4
+
5
+ ## Languages
6
+
7
+ **Primary:**
8
+ - JavaScript (ES Modules) - Node.js runtime
9
+ - TypeScript (not detected in codebase, but Dockerfile references Node.js)
10
+
11
+ **Secondary:**
12
+ - Dockerfile uses shell scripts
13
+ - HTML served for setup UI
14
+
15
+ ## Runtime
16
+
17
+ **Environment:**
18
+ - Node.js >=22 (enforced in package.json)
19
+ - Railway deployment platform
20
+ - Docker containerized
21
+
22
+ **Package Manager:**
23
+ - npm (used for wrapper dependencies)
24
+ - pnpm (used for clawdbot build dependencies - not in this repo)
25
+
26
+ **Type System:**
27
+ - JavaScript with ES Modules
28
+ - No TypeScript compilation detected
29
+
30
+ ## Frameworks
31
+
32
+ **Core:**
33
+ - Express.js ^5.1.0 - HTTP server and routing
34
+ - Native Node.js modules for core functionality
35
+
36
+ **Testing:**
37
+ - No testing framework detected
38
+ - Custom smoke test script
39
+
40
+ **Build/Dev:**
41
+ - Docker multi-stage build
42
+ - Railway.toml for deployment configuration
43
+ - GitHub Actions for CI/CD
44
+
45
+ ## Key Dependencies
46
+
47
+ **Critical:**
48
+ - express ^5.1.0 - Web server framework
49
+ - http-proxy ^1.18.1 - HTTP proxying to clawdbot gateway
50
+ - tar ^7.5.4 - Archive creation for backups
51
+
52
+ **Infrastructure:**
53
+ - Node.js runtime >=22
54
+ - Docker for containerization
55
+
56
+ ## Configuration
57
+
58
+ **Environment:**
59
+ - Railway deployment variables (PORT, CLAWDBOT_PUBLIC_PORT)
60
+ - Setup password protection (SETUP_PASSWORD)
61
+ - Gateway token management (CLAWDBOT_GATEWAY_TOKEN)
62
+ - State and workspace directories (CLAWDBOT_STATE_DIR, CLAWDBOT_WORKSPACE_DIR)
63
+
64
+ **Build:**
65
+ - Dockerfile for multi-stage build
66
+ - railway.toml for Railway deployment configuration
67
+ - package.json for dependency management
68
+
69
+ ## Platform Requirements
70
+
71
+ **Development:**
72
+ - Node.js >=22
73
+ - npm package manager
74
+
75
+ **Production:**
76
+ - Railway platform deployment
77
+ - Docker container runtime
78
+ - HTTP proxy setup
79
+
80
+ ---
81
+
82
+ *Stack analysis: 2026-01-29*
83
+ ```
.planning/codebase/STRUCTURE.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Codebase Structure
2
+
3
+ **Analysis Date:** 2026-01-29
4
+
5
+ ## Directory Layout
6
+
7
+ ```
8
+ clawdbot-railway-template/
9
+ ├── .github/ # CI/CD workflows
10
+ │ └── workflows/
11
+ │ └── docker-build.yml
12
+ ├── src/ # Application source
13
+ │ ├── server.js # Main server and proxy logic
14
+ │ └── setup-app.js # Setup wizard client
15
+ ├── scripts/ # Utility scripts
16
+ │ └── smoke.js # Basic sanity check
17
+ ├── .planning/ # Analysis documents (auto-generated)
18
+ │ └── codebase/
19
+ ├── Dockerfile # Multi-stage container build
20
+ ├── package.json # Node.js dependencies
21
+ ├── railway.toml # Railway deployment config
22
+ └── README.md # Project documentation
23
+ ```
24
+
25
+ ## Directory Purposes
26
+
27
+ **Root Directory:**
28
+ - Purpose: Project root containing configuration and deployment files
29
+ - Contains: Dockerfile, package.json, CI config, Railway config
30
+ - Key files: `package.json`, `Dockerfile`, `.github/workflows/docker-build.yml`
31
+
32
+ **`src/` Directory:**
33
+ - Purpose: Application source code
34
+ - Contains: Express server and setup client
35
+ - Key files: `src/server.js` (main server), `src/setup-app.js` (setup UI)
36
+
37
+ **`scripts/` Directory:**
38
+ - Purpose: Development and maintenance scripts
39
+ - Contains: Testing and verification utilities
40
+ - Key files: `scripts/smoke.js` (sanity check)
41
+
42
+ **`.github/workflows/` Directory:**
43
+ - Purpose: Continuous integration and deployment
44
+ - Contains: Docker build workflow
45
+ - Key files: `.github/workflows/docker-build.yml`
46
+
47
+ ## Key File Locations
48
+
49
+ **Entry Points:**
50
+ - `src/server.js`: Main application entry point
51
+ - `src/setup-app.js`: Setup wizard client-side code
52
+ - `scripts/smoke.js`: Development/testing utility
53
+
54
+ **Configuration:**
55
+ - `package.json`: Node.js dependencies and scripts
56
+ - `Dockerfile`: Container build configuration
57
+ - `.github/workflows/docker-build.yml`: CI/CD pipeline
58
+
59
+ **Core Logic:**
60
+ - `src/server.js`: All server, proxy, and gateway management logic
61
+ - `src/setup-app.js`: Client-side setup wizard functionality
62
+
63
+ ## Naming Conventions
64
+
65
+ **Files:**
66
+ - server.js: Main server application
67
+ - setup-app.js: Setup wizard client
68
+ - snake_case for configuration files (package.json, docker-build.yml)
69
+
70
+ **Variables:**
71
+ - PORT, STATE_DIR, WORKSPACE_DIR: Environment constants
72
+ - CLAWDBOT_*: Clawdbot-specific environment variables
73
+ - gatewayProc: Process management variables
74
+ - camelCase for function names and local variables
75
+
76
+ ## Where to Add New Code
77
+
78
+ **New Feature:**
79
+ - Server endpoints: Add to `src/server.js`
80
+ - Setup UI additions: Modify `src/server.html` (inline) and `src/setup-app.js`
81
+
82
+ **New Channel Integration:**
83
+ - Setup API: Add to `/setup/api/run` endpoint in `src/server.js`
84
+ - Client updates: Modify `src/setup-app.js` form and payload
85
+
86
+ **New Configuration Option:**
87
+ - Environment variables: Add to `src/server.js` configuration section
88
+ - Client support: Update `src/setup-app.js` if UI needed
89
+
90
+ **Utilities and Helpers:**
91
+ - Server utilities: Add to `src/server.js` or separate module if complex
92
+ - Client utilities: Add to `src/setup-app.js` or separate module
93
+ - Build/deployment scripts: Add to `scripts/` directory
94
+
95
+ ## Special Directories
96
+
97
+ **`.planning/codebase/`:**
98
+ - Purpose: Auto-generated analysis documents
99
+ - Generated: Yes
100
+ - Committed: Yes (for tracking)
101
+
102
+ **`src/`:**
103
+ - Purpose: Application source code
104
+ - Generated: No
105
+ - Committed: Yes
106
+
107
+ **`.github/workflows/`:**
108
+ - Purpose: CI/CD configuration
109
+ - Generated: No
110
+ - Committed: Yes
111
+
112
+ ---
113
+
114
+ *Structure analysis: 2026-01-29*
.planning/codebase/TESTING.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Testing Patterns
2
+
3
+ **Analysis Date:** 2024-01-29
4
+
5
+ ## Test Framework
6
+
7
+ **Runner:**
8
+ - No test framework detected
9
+ - No test scripts in package.json
10
+
11
+ **Assertion Library:**
12
+ - Not used
13
+ - Manual testing only
14
+
15
+ **Run Commands:**
16
+ ```bash
17
+ # No automated testing available
18
+ npm run lint # Basic syntax check only
19
+ npm run smoke # Basic CLI sanity check
20
+ ```
21
+
22
+ ## Test File Organization
23
+
24
+ **Location:**
25
+ - No test directory structure
26
+ - No test files present
27
+
28
+ **Naming:**
29
+ - No test naming convention detected
30
+
31
+ **Structure:**
32
+ - No test structure
33
+
34
+ ## Test Structure
35
+
36
+ **Suite Organization:**
37
+ - Not applicable
38
+
39
+ **Patterns:**
40
+ - Not applicable
41
+
42
+ ## Mocking
43
+
44
+ **Framework:** Not used
45
+
46
+ **Patterns:**
47
+ - Not applicable
48
+
49
+ **What to Mock:**
50
+ - Not applicable
51
+
52
+ **What NOT to Mock:**
53
+ - Not applicable
54
+
55
+ ## Fixtures and Factories
56
+
57
+ **Test Data:**
58
+ - Not applicable
59
+
60
+ **Location:**
61
+ - Not applicable
62
+
63
+ ## Coverage
64
+
65
+ **Requirements:** Not enforced
66
+
67
+ **View Coverage:**
68
+ - No coverage tool available
69
+ - No coverage reports
70
+
71
+ ## Test Types
72
+
73
+ **Unit Tests:**
74
+ - Not present
75
+
76
+ **Integration Tests:**
77
+ - Not present
78
+
79
+ **E2E Tests:**
80
+ - Not present
81
+
82
+ ## Common Patterns
83
+
84
+ **Async Testing:**
85
+ - Not applicable
86
+
87
+ **Error Testing:**
88
+ - Not applicable
89
+
90
+ ## Manual Testing Approach
91
+
92
+ The codebase relies on manual testing with the following patterns:
93
+
94
+ **Smoke Testing:**
95
+ - `scripts/smoke.js` verifies CLI is accessible
96
+ - Basic command: `clawdbot --version`
97
+
98
+ **Functional Testing:**
99
+ - Manual verification of web interface at `/setup`
100
+ - Manual verification of proxy functionality
101
+ - Manual channel setup verification
102
+
103
+ **Health Checks:**
104
+ - `/setup/healthz` endpoint for basic availability
105
+ - Gateway readiness checks in `waitForGatewayReady()`
106
+
107
+ **Setup Flow Testing:**
108
+ - Manual walkthrough of web-based setup wizard
109
+ - Configuration verification via API endpoints
110
+
111
+ **Gateway Testing:**
112
+ - Manual verification of proxy to internal gateway
113
+ - WebSocket proxy functionality
114
+ - Error handling when gateway is unavailable
115
+
116
+ ---
117
+
118
+ *Testing analysis: 2024-01-29*
119
+ *Note: No automated testing - entirely manual approach*
Dockerfile ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build moltbot from source to avoid npm packaging gaps (some dist files are not shipped).
2
+ FROM node:22-bookworm AS moltbot-build
3
+
4
+ # Dependencies needed for moltbot build
5
+ RUN apt-get update \
6
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
7
+ git \
8
+ ca-certificates \
9
+ curl \
10
+ python3 \
11
+ make \
12
+ g++ \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Install Bun (moltbot build uses it)
16
+ RUN curl -fsSL https://bun.sh/install | bash
17
+ ENV PATH="/root/.bun/bin:${PATH}"
18
+
19
+ RUN corepack enable
20
+
21
+ WORKDIR /moltbot
22
+
23
+ # Pin to a known ref (tag/branch). If it doesn't exist, fall back to main.
24
+ ARG MOLTBOT_GIT_REF=main
25
+ RUN git clone --depth 1 --branch "${MOLTBOT_GIT_REF}" https://github.com/moltbot/moltbot.git .
26
+
27
+ # Patch: relax version requirements for packages that may reference unpublished versions.
28
+ # Apply to all extension package.json files to handle workspace protocol (workspace:*).
29
+ RUN set -eux; \
30
+ find ./extensions -name 'package.json' -type f | while read -r f; do \
31
+ sed -i -E 's/"moltbot"[[:space:]]*:[[:space:]]*">=[^"]+"/"moltbot": "*"/g' "$f"; \
32
+ sed -i -E 's/"moltbot"[[:space:]]*:[[:space:]]*"workspace:[^"]+"/"moltbot": "*"/g' "$f"; \
33
+ done
34
+
35
+ RUN pnpm install --no-frozen-lockfile
36
+ RUN pnpm build
37
+ ENV MOLTBOT_PREFER_PNPM=1
38
+ RUN pnpm ui:install && pnpm ui:build
39
+
40
+
41
+ # Runtime image
42
+ FROM node:22-bookworm
43
+ ENV NODE_ENV=production
44
+
45
+ RUN apt-get update \
46
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
47
+ ca-certificates \
48
+ && rm -rf /var/lib/apt/lists/*
49
+
50
+ WORKDIR /app
51
+
52
+ # Wrapper deps
53
+ COPY package.json ./
54
+ RUN npm install --omit=dev && npm cache clean --force
55
+
56
+ # Copy built moltbot
57
+ COPY --from=moltbot-build /moltbot /moltbot
58
+
59
+ # Provide a moltbot executable
60
+ RUN printf '%s\n' '#!/usr/bin/env bash' 'exec node /moltbot/dist/entry.js "$@"' > /usr/local/bin/moltbot \
61
+ && chmod +x /usr/local/bin/moltbot
62
+
63
+ COPY src ./src
64
+
65
+ # The wrapper listens on this port.
66
+ ENV MOLTBOT_PUBLIC_PORT=8080
67
+ ENV PORT=8080
68
+ EXPOSE 8080
69
+ CMD ["node", "src/server.js"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vignesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package-lock.json ADDED
@@ -0,0 +1,942 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "clawdbot-railway-template",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "clawdbot-railway-template",
8
+ "dependencies": {
9
+ "express": "^5.1.0",
10
+ "http-proxy": "^1.18.1",
11
+ "tar": "^7.5.4"
12
+ },
13
+ "engines": {
14
+ "node": ">=22"
15
+ }
16
+ },
17
+ "node_modules/@isaacs/fs-minipass": {
18
+ "version": "4.0.1",
19
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
20
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "minipass": "^7.0.4"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ },
29
+ "node_modules/accepts": {
30
+ "version": "2.0.0",
31
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
32
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "mime-types": "^3.0.0",
36
+ "negotiator": "^1.0.0"
37
+ },
38
+ "engines": {
39
+ "node": ">= 0.6"
40
+ }
41
+ },
42
+ "node_modules/body-parser": {
43
+ "version": "2.2.2",
44
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
45
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "bytes": "^3.1.2",
49
+ "content-type": "^1.0.5",
50
+ "debug": "^4.4.3",
51
+ "http-errors": "^2.0.0",
52
+ "iconv-lite": "^0.7.0",
53
+ "on-finished": "^2.4.1",
54
+ "qs": "^6.14.1",
55
+ "raw-body": "^3.0.1",
56
+ "type-is": "^2.0.1"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "funding": {
62
+ "type": "opencollective",
63
+ "url": "https://opencollective.com/express"
64
+ }
65
+ },
66
+ "node_modules/bytes": {
67
+ "version": "3.1.2",
68
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
69
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
70
+ "license": "MIT",
71
+ "engines": {
72
+ "node": ">= 0.8"
73
+ }
74
+ },
75
+ "node_modules/call-bind-apply-helpers": {
76
+ "version": "1.0.2",
77
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
78
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
79
+ "license": "MIT",
80
+ "dependencies": {
81
+ "es-errors": "^1.3.0",
82
+ "function-bind": "^1.1.2"
83
+ },
84
+ "engines": {
85
+ "node": ">= 0.4"
86
+ }
87
+ },
88
+ "node_modules/call-bound": {
89
+ "version": "1.0.4",
90
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
91
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
92
+ "license": "MIT",
93
+ "dependencies": {
94
+ "call-bind-apply-helpers": "^1.0.2",
95
+ "get-intrinsic": "^1.3.0"
96
+ },
97
+ "engines": {
98
+ "node": ">= 0.4"
99
+ },
100
+ "funding": {
101
+ "url": "https://github.com/sponsors/ljharb"
102
+ }
103
+ },
104
+ "node_modules/chownr": {
105
+ "version": "3.0.0",
106
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
107
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
108
+ "license": "BlueOak-1.0.0",
109
+ "engines": {
110
+ "node": ">=18"
111
+ }
112
+ },
113
+ "node_modules/content-disposition": {
114
+ "version": "1.0.1",
115
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
116
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
117
+ "license": "MIT",
118
+ "engines": {
119
+ "node": ">=18"
120
+ },
121
+ "funding": {
122
+ "type": "opencollective",
123
+ "url": "https://opencollective.com/express"
124
+ }
125
+ },
126
+ "node_modules/content-type": {
127
+ "version": "1.0.5",
128
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
129
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
130
+ "license": "MIT",
131
+ "engines": {
132
+ "node": ">= 0.6"
133
+ }
134
+ },
135
+ "node_modules/cookie": {
136
+ "version": "0.7.2",
137
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
138
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
139
+ "license": "MIT",
140
+ "engines": {
141
+ "node": ">= 0.6"
142
+ }
143
+ },
144
+ "node_modules/cookie-signature": {
145
+ "version": "1.2.2",
146
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
147
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
148
+ "license": "MIT",
149
+ "engines": {
150
+ "node": ">=6.6.0"
151
+ }
152
+ },
153
+ "node_modules/debug": {
154
+ "version": "4.4.3",
155
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
156
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
157
+ "license": "MIT",
158
+ "dependencies": {
159
+ "ms": "^2.1.3"
160
+ },
161
+ "engines": {
162
+ "node": ">=6.0"
163
+ },
164
+ "peerDependenciesMeta": {
165
+ "supports-color": {
166
+ "optional": true
167
+ }
168
+ }
169
+ },
170
+ "node_modules/depd": {
171
+ "version": "2.0.0",
172
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
173
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
174
+ "license": "MIT",
175
+ "engines": {
176
+ "node": ">= 0.8"
177
+ }
178
+ },
179
+ "node_modules/dunder-proto": {
180
+ "version": "1.0.1",
181
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
182
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
183
+ "license": "MIT",
184
+ "dependencies": {
185
+ "call-bind-apply-helpers": "^1.0.1",
186
+ "es-errors": "^1.3.0",
187
+ "gopd": "^1.2.0"
188
+ },
189
+ "engines": {
190
+ "node": ">= 0.4"
191
+ }
192
+ },
193
+ "node_modules/ee-first": {
194
+ "version": "1.1.1",
195
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
196
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
197
+ "license": "MIT"
198
+ },
199
+ "node_modules/encodeurl": {
200
+ "version": "2.0.0",
201
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
202
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
203
+ "license": "MIT",
204
+ "engines": {
205
+ "node": ">= 0.8"
206
+ }
207
+ },
208
+ "node_modules/es-define-property": {
209
+ "version": "1.0.1",
210
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
211
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
212
+ "license": "MIT",
213
+ "engines": {
214
+ "node": ">= 0.4"
215
+ }
216
+ },
217
+ "node_modules/es-errors": {
218
+ "version": "1.3.0",
219
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
220
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
221
+ "license": "MIT",
222
+ "engines": {
223
+ "node": ">= 0.4"
224
+ }
225
+ },
226
+ "node_modules/es-object-atoms": {
227
+ "version": "1.1.1",
228
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
229
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
230
+ "license": "MIT",
231
+ "dependencies": {
232
+ "es-errors": "^1.3.0"
233
+ },
234
+ "engines": {
235
+ "node": ">= 0.4"
236
+ }
237
+ },
238
+ "node_modules/escape-html": {
239
+ "version": "1.0.3",
240
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
241
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
242
+ "license": "MIT"
243
+ },
244
+ "node_modules/etag": {
245
+ "version": "1.8.1",
246
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
247
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
248
+ "license": "MIT",
249
+ "engines": {
250
+ "node": ">= 0.6"
251
+ }
252
+ },
253
+ "node_modules/eventemitter3": {
254
+ "version": "4.0.7",
255
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
256
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
257
+ "license": "MIT"
258
+ },
259
+ "node_modules/express": {
260
+ "version": "5.2.1",
261
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
262
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
263
+ "license": "MIT",
264
+ "dependencies": {
265
+ "accepts": "^2.0.0",
266
+ "body-parser": "^2.2.1",
267
+ "content-disposition": "^1.0.0",
268
+ "content-type": "^1.0.5",
269
+ "cookie": "^0.7.1",
270
+ "cookie-signature": "^1.2.1",
271
+ "debug": "^4.4.0",
272
+ "depd": "^2.0.0",
273
+ "encodeurl": "^2.0.0",
274
+ "escape-html": "^1.0.3",
275
+ "etag": "^1.8.1",
276
+ "finalhandler": "^2.1.0",
277
+ "fresh": "^2.0.0",
278
+ "http-errors": "^2.0.0",
279
+ "merge-descriptors": "^2.0.0",
280
+ "mime-types": "^3.0.0",
281
+ "on-finished": "^2.4.1",
282
+ "once": "^1.4.0",
283
+ "parseurl": "^1.3.3",
284
+ "proxy-addr": "^2.0.7",
285
+ "qs": "^6.14.0",
286
+ "range-parser": "^1.2.1",
287
+ "router": "^2.2.0",
288
+ "send": "^1.1.0",
289
+ "serve-static": "^2.2.0",
290
+ "statuses": "^2.0.1",
291
+ "type-is": "^2.0.1",
292
+ "vary": "^1.1.2"
293
+ },
294
+ "engines": {
295
+ "node": ">= 18"
296
+ },
297
+ "funding": {
298
+ "type": "opencollective",
299
+ "url": "https://opencollective.com/express"
300
+ }
301
+ },
302
+ "node_modules/finalhandler": {
303
+ "version": "2.1.1",
304
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
305
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
306
+ "license": "MIT",
307
+ "dependencies": {
308
+ "debug": "^4.4.0",
309
+ "encodeurl": "^2.0.0",
310
+ "escape-html": "^1.0.3",
311
+ "on-finished": "^2.4.1",
312
+ "parseurl": "^1.3.3",
313
+ "statuses": "^2.0.1"
314
+ },
315
+ "engines": {
316
+ "node": ">= 18.0.0"
317
+ },
318
+ "funding": {
319
+ "type": "opencollective",
320
+ "url": "https://opencollective.com/express"
321
+ }
322
+ },
323
+ "node_modules/follow-redirects": {
324
+ "version": "1.15.11",
325
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
326
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
327
+ "funding": [
328
+ {
329
+ "type": "individual",
330
+ "url": "https://github.com/sponsors/RubenVerborgh"
331
+ }
332
+ ],
333
+ "license": "MIT",
334
+ "engines": {
335
+ "node": ">=4.0"
336
+ },
337
+ "peerDependenciesMeta": {
338
+ "debug": {
339
+ "optional": true
340
+ }
341
+ }
342
+ },
343
+ "node_modules/forwarded": {
344
+ "version": "0.2.0",
345
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
346
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
347
+ "license": "MIT",
348
+ "engines": {
349
+ "node": ">= 0.6"
350
+ }
351
+ },
352
+ "node_modules/fresh": {
353
+ "version": "2.0.0",
354
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
355
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
356
+ "license": "MIT",
357
+ "engines": {
358
+ "node": ">= 0.8"
359
+ }
360
+ },
361
+ "node_modules/function-bind": {
362
+ "version": "1.1.2",
363
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
364
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
365
+ "license": "MIT",
366
+ "funding": {
367
+ "url": "https://github.com/sponsors/ljharb"
368
+ }
369
+ },
370
+ "node_modules/get-intrinsic": {
371
+ "version": "1.3.0",
372
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
373
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
374
+ "license": "MIT",
375
+ "dependencies": {
376
+ "call-bind-apply-helpers": "^1.0.2",
377
+ "es-define-property": "^1.0.1",
378
+ "es-errors": "^1.3.0",
379
+ "es-object-atoms": "^1.1.1",
380
+ "function-bind": "^1.1.2",
381
+ "get-proto": "^1.0.1",
382
+ "gopd": "^1.2.0",
383
+ "has-symbols": "^1.1.0",
384
+ "hasown": "^2.0.2",
385
+ "math-intrinsics": "^1.1.0"
386
+ },
387
+ "engines": {
388
+ "node": ">= 0.4"
389
+ },
390
+ "funding": {
391
+ "url": "https://github.com/sponsors/ljharb"
392
+ }
393
+ },
394
+ "node_modules/get-proto": {
395
+ "version": "1.0.1",
396
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
397
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
398
+ "license": "MIT",
399
+ "dependencies": {
400
+ "dunder-proto": "^1.0.1",
401
+ "es-object-atoms": "^1.0.0"
402
+ },
403
+ "engines": {
404
+ "node": ">= 0.4"
405
+ }
406
+ },
407
+ "node_modules/gopd": {
408
+ "version": "1.2.0",
409
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
410
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
411
+ "license": "MIT",
412
+ "engines": {
413
+ "node": ">= 0.4"
414
+ },
415
+ "funding": {
416
+ "url": "https://github.com/sponsors/ljharb"
417
+ }
418
+ },
419
+ "node_modules/has-symbols": {
420
+ "version": "1.1.0",
421
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
422
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
423
+ "license": "MIT",
424
+ "engines": {
425
+ "node": ">= 0.4"
426
+ },
427
+ "funding": {
428
+ "url": "https://github.com/sponsors/ljharb"
429
+ }
430
+ },
431
+ "node_modules/hasown": {
432
+ "version": "2.0.2",
433
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
434
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
435
+ "license": "MIT",
436
+ "dependencies": {
437
+ "function-bind": "^1.1.2"
438
+ },
439
+ "engines": {
440
+ "node": ">= 0.4"
441
+ }
442
+ },
443
+ "node_modules/http-errors": {
444
+ "version": "2.0.1",
445
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
446
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
447
+ "license": "MIT",
448
+ "dependencies": {
449
+ "depd": "~2.0.0",
450
+ "inherits": "~2.0.4",
451
+ "setprototypeof": "~1.2.0",
452
+ "statuses": "~2.0.2",
453
+ "toidentifier": "~1.0.1"
454
+ },
455
+ "engines": {
456
+ "node": ">= 0.8"
457
+ },
458
+ "funding": {
459
+ "type": "opencollective",
460
+ "url": "https://opencollective.com/express"
461
+ }
462
+ },
463
+ "node_modules/http-proxy": {
464
+ "version": "1.18.1",
465
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
466
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "eventemitter3": "^4.0.0",
470
+ "follow-redirects": "^1.0.0",
471
+ "requires-port": "^1.0.0"
472
+ },
473
+ "engines": {
474
+ "node": ">=8.0.0"
475
+ }
476
+ },
477
+ "node_modules/iconv-lite": {
478
+ "version": "0.7.2",
479
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
480
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
481
+ "license": "MIT",
482
+ "dependencies": {
483
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
484
+ },
485
+ "engines": {
486
+ "node": ">=0.10.0"
487
+ },
488
+ "funding": {
489
+ "type": "opencollective",
490
+ "url": "https://opencollective.com/express"
491
+ }
492
+ },
493
+ "node_modules/inherits": {
494
+ "version": "2.0.4",
495
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
496
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
497
+ "license": "ISC"
498
+ },
499
+ "node_modules/ipaddr.js": {
500
+ "version": "1.9.1",
501
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
502
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
503
+ "license": "MIT",
504
+ "engines": {
505
+ "node": ">= 0.10"
506
+ }
507
+ },
508
+ "node_modules/is-promise": {
509
+ "version": "4.0.0",
510
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
511
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
512
+ "license": "MIT"
513
+ },
514
+ "node_modules/math-intrinsics": {
515
+ "version": "1.1.0",
516
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
517
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
518
+ "license": "MIT",
519
+ "engines": {
520
+ "node": ">= 0.4"
521
+ }
522
+ },
523
+ "node_modules/media-typer": {
524
+ "version": "1.1.0",
525
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
526
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
527
+ "license": "MIT",
528
+ "engines": {
529
+ "node": ">= 0.8"
530
+ }
531
+ },
532
+ "node_modules/merge-descriptors": {
533
+ "version": "2.0.0",
534
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
535
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
536
+ "license": "MIT",
537
+ "engines": {
538
+ "node": ">=18"
539
+ },
540
+ "funding": {
541
+ "url": "https://github.com/sponsors/sindresorhus"
542
+ }
543
+ },
544
+ "node_modules/mime-db": {
545
+ "version": "1.54.0",
546
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
547
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
548
+ "license": "MIT",
549
+ "engines": {
550
+ "node": ">= 0.6"
551
+ }
552
+ },
553
+ "node_modules/mime-types": {
554
+ "version": "3.0.2",
555
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
556
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
557
+ "license": "MIT",
558
+ "dependencies": {
559
+ "mime-db": "^1.54.0"
560
+ },
561
+ "engines": {
562
+ "node": ">=18"
563
+ },
564
+ "funding": {
565
+ "type": "opencollective",
566
+ "url": "https://opencollective.com/express"
567
+ }
568
+ },
569
+ "node_modules/minipass": {
570
+ "version": "7.1.2",
571
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
572
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
573
+ "license": "ISC",
574
+ "engines": {
575
+ "node": ">=16 || 14 >=14.17"
576
+ }
577
+ },
578
+ "node_modules/minizlib": {
579
+ "version": "3.1.0",
580
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
581
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
582
+ "license": "MIT",
583
+ "dependencies": {
584
+ "minipass": "^7.1.2"
585
+ },
586
+ "engines": {
587
+ "node": ">= 18"
588
+ }
589
+ },
590
+ "node_modules/ms": {
591
+ "version": "2.1.3",
592
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
593
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
594
+ "license": "MIT"
595
+ },
596
+ "node_modules/negotiator": {
597
+ "version": "1.0.0",
598
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
599
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
600
+ "license": "MIT",
601
+ "engines": {
602
+ "node": ">= 0.6"
603
+ }
604
+ },
605
+ "node_modules/object-inspect": {
606
+ "version": "1.13.4",
607
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
608
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
609
+ "license": "MIT",
610
+ "engines": {
611
+ "node": ">= 0.4"
612
+ },
613
+ "funding": {
614
+ "url": "https://github.com/sponsors/ljharb"
615
+ }
616
+ },
617
+ "node_modules/on-finished": {
618
+ "version": "2.4.1",
619
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
620
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
621
+ "license": "MIT",
622
+ "dependencies": {
623
+ "ee-first": "1.1.1"
624
+ },
625
+ "engines": {
626
+ "node": ">= 0.8"
627
+ }
628
+ },
629
+ "node_modules/once": {
630
+ "version": "1.4.0",
631
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
632
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
633
+ "license": "ISC",
634
+ "dependencies": {
635
+ "wrappy": "1"
636
+ }
637
+ },
638
+ "node_modules/parseurl": {
639
+ "version": "1.3.3",
640
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
641
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
642
+ "license": "MIT",
643
+ "engines": {
644
+ "node": ">= 0.8"
645
+ }
646
+ },
647
+ "node_modules/path-to-regexp": {
648
+ "version": "8.3.0",
649
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
650
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
651
+ "license": "MIT",
652
+ "funding": {
653
+ "type": "opencollective",
654
+ "url": "https://opencollective.com/express"
655
+ }
656
+ },
657
+ "node_modules/proxy-addr": {
658
+ "version": "2.0.7",
659
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
660
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
661
+ "license": "MIT",
662
+ "dependencies": {
663
+ "forwarded": "0.2.0",
664
+ "ipaddr.js": "1.9.1"
665
+ },
666
+ "engines": {
667
+ "node": ">= 0.10"
668
+ }
669
+ },
670
+ "node_modules/qs": {
671
+ "version": "6.14.1",
672
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
673
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
674
+ "license": "BSD-3-Clause",
675
+ "dependencies": {
676
+ "side-channel": "^1.1.0"
677
+ },
678
+ "engines": {
679
+ "node": ">=0.6"
680
+ },
681
+ "funding": {
682
+ "url": "https://github.com/sponsors/ljharb"
683
+ }
684
+ },
685
+ "node_modules/range-parser": {
686
+ "version": "1.2.1",
687
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
688
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
689
+ "license": "MIT",
690
+ "engines": {
691
+ "node": ">= 0.6"
692
+ }
693
+ },
694
+ "node_modules/raw-body": {
695
+ "version": "3.0.2",
696
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
697
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
698
+ "license": "MIT",
699
+ "dependencies": {
700
+ "bytes": "~3.1.2",
701
+ "http-errors": "~2.0.1",
702
+ "iconv-lite": "~0.7.0",
703
+ "unpipe": "~1.0.0"
704
+ },
705
+ "engines": {
706
+ "node": ">= 0.10"
707
+ }
708
+ },
709
+ "node_modules/requires-port": {
710
+ "version": "1.0.0",
711
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
712
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
713
+ "license": "MIT"
714
+ },
715
+ "node_modules/router": {
716
+ "version": "2.2.0",
717
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
718
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
719
+ "license": "MIT",
720
+ "dependencies": {
721
+ "debug": "^4.4.0",
722
+ "depd": "^2.0.0",
723
+ "is-promise": "^4.0.0",
724
+ "parseurl": "^1.3.3",
725
+ "path-to-regexp": "^8.0.0"
726
+ },
727
+ "engines": {
728
+ "node": ">= 18"
729
+ }
730
+ },
731
+ "node_modules/safer-buffer": {
732
+ "version": "2.1.2",
733
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
734
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
735
+ "license": "MIT"
736
+ },
737
+ "node_modules/send": {
738
+ "version": "1.2.1",
739
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
740
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
741
+ "license": "MIT",
742
+ "dependencies": {
743
+ "debug": "^4.4.3",
744
+ "encodeurl": "^2.0.0",
745
+ "escape-html": "^1.0.3",
746
+ "etag": "^1.8.1",
747
+ "fresh": "^2.0.0",
748
+ "http-errors": "^2.0.1",
749
+ "mime-types": "^3.0.2",
750
+ "ms": "^2.1.3",
751
+ "on-finished": "^2.4.1",
752
+ "range-parser": "^1.2.1",
753
+ "statuses": "^2.0.2"
754
+ },
755
+ "engines": {
756
+ "node": ">= 18"
757
+ },
758
+ "funding": {
759
+ "type": "opencollective",
760
+ "url": "https://opencollective.com/express"
761
+ }
762
+ },
763
+ "node_modules/serve-static": {
764
+ "version": "2.2.1",
765
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
766
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
767
+ "license": "MIT",
768
+ "dependencies": {
769
+ "encodeurl": "^2.0.0",
770
+ "escape-html": "^1.0.3",
771
+ "parseurl": "^1.3.3",
772
+ "send": "^1.2.0"
773
+ },
774
+ "engines": {
775
+ "node": ">= 18"
776
+ },
777
+ "funding": {
778
+ "type": "opencollective",
779
+ "url": "https://opencollective.com/express"
780
+ }
781
+ },
782
+ "node_modules/setprototypeof": {
783
+ "version": "1.2.0",
784
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
785
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
786
+ "license": "ISC"
787
+ },
788
+ "node_modules/side-channel": {
789
+ "version": "1.1.0",
790
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
791
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
792
+ "license": "MIT",
793
+ "dependencies": {
794
+ "es-errors": "^1.3.0",
795
+ "object-inspect": "^1.13.3",
796
+ "side-channel-list": "^1.0.0",
797
+ "side-channel-map": "^1.0.1",
798
+ "side-channel-weakmap": "^1.0.2"
799
+ },
800
+ "engines": {
801
+ "node": ">= 0.4"
802
+ },
803
+ "funding": {
804
+ "url": "https://github.com/sponsors/ljharb"
805
+ }
806
+ },
807
+ "node_modules/side-channel-list": {
808
+ "version": "1.0.0",
809
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
810
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
811
+ "license": "MIT",
812
+ "dependencies": {
813
+ "es-errors": "^1.3.0",
814
+ "object-inspect": "^1.13.3"
815
+ },
816
+ "engines": {
817
+ "node": ">= 0.4"
818
+ },
819
+ "funding": {
820
+ "url": "https://github.com/sponsors/ljharb"
821
+ }
822
+ },
823
+ "node_modules/side-channel-map": {
824
+ "version": "1.0.1",
825
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
826
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
827
+ "license": "MIT",
828
+ "dependencies": {
829
+ "call-bound": "^1.0.2",
830
+ "es-errors": "^1.3.0",
831
+ "get-intrinsic": "^1.2.5",
832
+ "object-inspect": "^1.13.3"
833
+ },
834
+ "engines": {
835
+ "node": ">= 0.4"
836
+ },
837
+ "funding": {
838
+ "url": "https://github.com/sponsors/ljharb"
839
+ }
840
+ },
841
+ "node_modules/side-channel-weakmap": {
842
+ "version": "1.0.2",
843
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
844
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
845
+ "license": "MIT",
846
+ "dependencies": {
847
+ "call-bound": "^1.0.2",
848
+ "es-errors": "^1.3.0",
849
+ "get-intrinsic": "^1.2.5",
850
+ "object-inspect": "^1.13.3",
851
+ "side-channel-map": "^1.0.1"
852
+ },
853
+ "engines": {
854
+ "node": ">= 0.4"
855
+ },
856
+ "funding": {
857
+ "url": "https://github.com/sponsors/ljharb"
858
+ }
859
+ },
860
+ "node_modules/statuses": {
861
+ "version": "2.0.2",
862
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
863
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
864
+ "license": "MIT",
865
+ "engines": {
866
+ "node": ">= 0.8"
867
+ }
868
+ },
869
+ "node_modules/tar": {
870
+ "version": "7.5.6",
871
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
872
+ "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
873
+ "license": "BlueOak-1.0.0",
874
+ "dependencies": {
875
+ "@isaacs/fs-minipass": "^4.0.0",
876
+ "chownr": "^3.0.0",
877
+ "minipass": "^7.1.2",
878
+ "minizlib": "^3.1.0",
879
+ "yallist": "^5.0.0"
880
+ },
881
+ "engines": {
882
+ "node": ">=18"
883
+ }
884
+ },
885
+ "node_modules/toidentifier": {
886
+ "version": "1.0.1",
887
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
888
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
889
+ "license": "MIT",
890
+ "engines": {
891
+ "node": ">=0.6"
892
+ }
893
+ },
894
+ "node_modules/type-is": {
895
+ "version": "2.0.1",
896
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
897
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
898
+ "license": "MIT",
899
+ "dependencies": {
900
+ "content-type": "^1.0.5",
901
+ "media-typer": "^1.1.0",
902
+ "mime-types": "^3.0.0"
903
+ },
904
+ "engines": {
905
+ "node": ">= 0.6"
906
+ }
907
+ },
908
+ "node_modules/unpipe": {
909
+ "version": "1.0.0",
910
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
911
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
912
+ "license": "MIT",
913
+ "engines": {
914
+ "node": ">= 0.8"
915
+ }
916
+ },
917
+ "node_modules/vary": {
918
+ "version": "1.1.2",
919
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
920
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
921
+ "license": "MIT",
922
+ "engines": {
923
+ "node": ">= 0.8"
924
+ }
925
+ },
926
+ "node_modules/wrappy": {
927
+ "version": "1.0.2",
928
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
929
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
930
+ "license": "ISC"
931
+ },
932
+ "node_modules/yallist": {
933
+ "version": "5.0.0",
934
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
935
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
936
+ "license": "BlueOak-1.0.0",
937
+ "engines": {
938
+ "node": ">=18"
939
+ }
940
+ }
941
+ }
942
+ }
package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "moltbot-railway-template",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
8
+ "scripts": {
9
+ "dev": "node src/server.js",
10
+ "start": "node src/server.js",
11
+ "lint": "node -c src/server.js",
12
+ "smoke": "node scripts/smoke.js"
13
+ },
14
+ "dependencies": {
15
+ "express": "^5.1.0",
16
+ "http-proxy": "^1.18.1",
17
+ "tar": "^7.5.4"
18
+ }
19
+ }
railway.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [build]
2
+ builder = "dockerfile"
3
+
4
+ [deploy]
5
+ healthcheckPath = "/setup/healthz"
6
+ healthcheckTimeout = 300
7
+ restartPolicyType = "on_failure"
8
+
9
+ [variables]
10
+ PORT = "8080"
scripts/smoke.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ // Basic sanity: ensure the wrapper starts and the CLI exists.
4
+ const r = spawnSync("moltbot", ["--version"], { encoding: "utf8" });
5
+ if (r.status !== 0) {
6
+ console.error(r.stdout || r.stderr);
7
+ process.exit(r.status ?? 1);
8
+ }
9
+ console.log("moltbot ok:", r.stdout.trim());
src/server.js ADDED
@@ -0,0 +1,1181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import childProcess from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import express from "express";
8
+ import httpProxy from "http-proxy";
9
+ import * as tar from "tar";
10
+
11
+ // Railway deployments sometimes inject PORT=3000 by default. We want the wrapper to
12
+ // reliably listen on 8080 unless explicitly overridden.
13
+ //
14
+ // Prefer MOLTBOT_PUBLIC_PORT (set in the Dockerfile / template) over PORT.
15
+ // Backwards compatible: also supports CLAWDBOT_PUBLIC_PORT.
16
+ const PORT = Number.parseInt(
17
+ process.env.MOLTBOT_PUBLIC_PORT ??
18
+ process.env.CLAWDBOT_PUBLIC_PORT ??
19
+ process.env.PORT ?? "8080", 10
20
+ );
21
+ const STATE_DIR =
22
+ process.env.MOLTBOT_STATE_DIR?.trim() ||
23
+ process.env.CLAWDBOT_STATE_DIR?.trim() ||
24
+ path.join(os.homedir(), ".moltbot");
25
+ const WORKSPACE_DIR =
26
+ process.env.MOLTBOT_WORKSPACE_DIR?.trim() ||
27
+ process.env.CLAWDBOT_WORKSPACE_DIR?.trim() ||
28
+ path.join(STATE_DIR, "workspace");
29
+
30
+ // Protect /setup with a user-provided password.
31
+ const SETUP_PASSWORD = process.env.SETUP_PASSWORD?.trim();
32
+
33
+ // Gateway admin token (protects Moltbot gateway + Control UI).
34
+ // Must be stable across restarts. If not provided via env, persist it in the state dir.
35
+ // Backwards compatible: also supports CLAWDBOT_GATEWAY_TOKEN.
36
+ function resolveGatewayToken() {
37
+ const envTok =
38
+ process.env.MOLTBOT_GATEWAY_TOKEN?.trim() ||
39
+ process.env.CLAWDBOT_GATEWAY_TOKEN?.trim();
40
+ if (envTok) return envTok;
41
+
42
+ const tokenPath = path.join(STATE_DIR, "gateway.token");
43
+ try {
44
+ const existing = fs.readFileSync(tokenPath, "utf8").trim();
45
+ if (existing) return existing;
46
+ } catch {
47
+ // ignore
48
+ }
49
+
50
+ const generated = crypto.randomBytes(32).toString("hex");
51
+ try {
52
+ fs.mkdirSync(STATE_DIR, { recursive: true });
53
+ fs.writeFileSync(tokenPath, generated, { encoding: "utf8", mode: 0o600 });
54
+ } catch {
55
+ // best-effort
56
+ }
57
+ return generated;
58
+ }
59
+
60
+ const MOLTBOT_GATEWAY_TOKEN = resolveGatewayToken();
61
+ process.env.MOLTBOT_GATEWAY_TOKEN = MOLTBOT_GATEWAY_TOKEN;
62
+ // Also set CLAWDBOT_GATEWAY_TOKEN for backwards compat with gateway
63
+ process.env.CLAWDBOT_GATEWAY_TOKEN = MOLTBOT_GATEWAY_TOKEN;
64
+
65
+ // Where the gateway will listen internally (we proxy to it).
66
+ const INTERNAL_GATEWAY_PORT = Number.parseInt(process.env.INTERNAL_GATEWAY_PORT ?? "18789", 10);
67
+ const INTERNAL_GATEWAY_HOST = process.env.INTERNAL_GATEWAY_HOST ?? "127.0.0.1";
68
+ const GATEWAY_TARGET = `http://${INTERNAL_GATEWAY_HOST}:${INTERNAL_GATEWAY_PORT}`;
69
+
70
+ // Always run the built-from-source CLI entry directly to avoid PATH/global-install mismatches.
71
+ // Backwards compatible: also supports CLAWDBOT_ENTRY and CLAWDBOT_NODE.
72
+ const MOLTBOT_ENTRY = process.env.MOLTBOT_ENTRY?.trim() || process.env.CLAWDBOT_ENTRY?.trim() || "/moltbot/dist/entry.js";
73
+ const MOLTBOT_NODE = process.env.MOLTBOT_NODE?.trim() || process.env.CLAWDBOT_NODE?.trim() || "node";
74
+
75
+ function moltArgs(args) {
76
+ return [MOLTBOT_ENTRY, ...args];
77
+ }
78
+
79
+ function configPath() {
80
+ return process.env.MOLTBOT_CONFIG_PATH?.trim() ||
81
+ process.env.CLAWDBOT_CONFIG_PATH?.trim() ||
82
+ path.join(STATE_DIR, "moltbot.json");
83
+ }
84
+
85
+ function isConfigured() {
86
+ try {
87
+ return fs.existsSync(configPath());
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ let gatewayProc = null;
94
+ let gatewayStarting = null;
95
+
96
+ function sleep(ms) {
97
+ return new Promise((r) => setTimeout(r, ms));
98
+ }
99
+
100
+ async function waitForGatewayReady(opts = {}) {
101
+ const timeoutMs = opts.timeoutMs ?? 20_000;
102
+ const start = Date.now();
103
+ while (Date.now() - start < timeoutMs) {
104
+ try {
105
+ const res = await fetch(`${GATEWAY_TARGET}/moltbot`, { method: "GET" });
106
+ // Any HTTP response means the port is open.
107
+ if (res) return true;
108
+ } catch {
109
+ // not ready
110
+ }
111
+ await sleep(250);
112
+ }
113
+ return false;
114
+ }
115
+
116
+ async function startGateway() {
117
+ if (gatewayProc) return;
118
+ if (!isConfigured()) throw new Error("Gateway cannot start: not configured");
119
+
120
+ fs.mkdirSync(STATE_DIR, { recursive: true });
121
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
122
+
123
+ const args = [
124
+ "gateway",
125
+ "run",
126
+ "--bind",
127
+ "loopback",
128
+ "--port",
129
+ String(INTERNAL_GATEWAY_PORT),
130
+ "--auth",
131
+ "token",
132
+ "--token",
133
+ MOLTBOT_GATEWAY_TOKEN,
134
+ ];
135
+
136
+ gatewayProc = childProcess.spawn(MOLTBOT_NODE, moltArgs(args), {
137
+ stdio: "inherit",
138
+ env: {
139
+ ...process.env,
140
+ MOLTBOT_STATE_DIR: STATE_DIR,
141
+ MOLTBOT_WORKSPACE_DIR: WORKSPACE_DIR,
142
+ // Also set CLAWDBOT_* for backwards compat
143
+ CLAWDBOT_STATE_DIR: STATE_DIR,
144
+ CLAWDBOT_WORKSPACE_DIR: WORKSPACE_DIR,
145
+ },
146
+ });
147
+
148
+ gatewayProc.on("error", (err) => {
149
+ console.error(`[gateway] spawn error: ${String(err)}`);
150
+ gatewayProc = null;
151
+ });
152
+
153
+ gatewayProc.on("exit", (code, signal) => {
154
+ console.error(`[gateway] exited code=${code} signal=${signal}`);
155
+ gatewayProc = null;
156
+ });
157
+ }
158
+
159
+ async function ensureGatewayRunning() {
160
+ if (!isConfigured()) return { ok: false, reason: "not configured" };
161
+ if (gatewayProc) return { ok: true };
162
+ if (!gatewayStarting) {
163
+ gatewayStarting = (async () => {
164
+ await startGateway();
165
+ const ready = await waitForGatewayReady({ timeoutMs: 20_000 });
166
+ if (!ready) {
167
+ throw new Error("Gateway did not become ready in time");
168
+ }
169
+ })().finally(() => {
170
+ gatewayStarting = null;
171
+ });
172
+ }
173
+ await gatewayStarting;
174
+ return { ok: true };
175
+ }
176
+
177
+ async function restartGateway() {
178
+ if (gatewayProc) {
179
+ try {
180
+ gatewayProc.kill("SIGTERM");
181
+ } catch {
182
+ // ignore
183
+ }
184
+ // Give it a moment to exit and release the port.
185
+ await sleep(750);
186
+ gatewayProc = null;
187
+ }
188
+ return ensureGatewayRunning();
189
+ }
190
+
191
+ function requireSetupAuth(req, res, next) {
192
+ if (!SETUP_PASSWORD) {
193
+ return res
194
+ .status(500)
195
+ .type("text/plain")
196
+ .send("SETUP_PASSWORD is not set. Set it in Railway Variables before using /setup.");
197
+ }
198
+
199
+ const header = req.headers.authorization || "";
200
+ const [scheme, encoded] = header.split(" ");
201
+ if (scheme !== "Basic" || !encoded) {
202
+ res.set("WWW-Authenticate", 'Basic realm="Moltbot Setup"');
203
+ return res.status(401).send("Auth required");
204
+ }
205
+ const decoded = Buffer.from(encoded, "base64").toString("utf8");
206
+ const idx = decoded.indexOf(":");
207
+ const password = idx >= 0 ? decoded.slice(idx + 1) : "";
208
+ if (password !== SETUP_PASSWORD) {
209
+ res.set("WWW-Authenticate", 'Basic realm="Moltbot Setup"');
210
+ return res.status(401).send("Invalid password");
211
+ }
212
+ return next();
213
+ }
214
+
215
+ const app = express();
216
+ app.disable("x-powered-by");
217
+ app.use(express.json({ limit: "1mb" }));
218
+
219
+ // Minimal health endpoint for Railway.
220
+ app.get("/setup/healthz", (_req, res) => res.json({ ok: true }));
221
+
222
+ // Provider templates for one-click setup
223
+ const PROVIDER_TEMPLATES = {
224
+ openai: {
225
+ name: "OpenAI",
226
+ description: "GPT-4 and GPT-3.5 models",
227
+ authChoice: "openai-api-key",
228
+ placeholder: "sk-...",
229
+ fields: {
230
+ authSecret: {
231
+ label: "API Key",
232
+ type: "password",
233
+ help: "Get your key from https://platform.openai.com/api-keys",
234
+ helpUrl: "https://platform.openai.com/api-keys"
235
+ }
236
+ },
237
+ icon: "🤖"
238
+ },
239
+ anthropic: {
240
+ name: "Anthropic Claude",
241
+ description: "Claude 3.5 Sonnet, Opus, and Haiku",
242
+ authChoice: "anthropic-api-key",
243
+ placeholder: "sk-ant-...",
244
+ fields: {
245
+ authSecret: {
246
+ label: "API Key",
247
+ type: "password",
248
+ help: "Get your key from https://console.anthropic.com/",
249
+ helpUrl: "https://console.anthropic.com/"
250
+ }
251
+ },
252
+ icon: "🧠"
253
+ },
254
+ google: {
255
+ name: "Google Gemini",
256
+ description: "Gemini Pro and Ultra models",
257
+ authChoice: "gemini-api-key",
258
+ placeholder: "AIza...",
259
+ fields: {
260
+ authSecret: {
261
+ label: "API Key",
262
+ type: "password",
263
+ help: "Get your key from https://makersuite.google.com/app/apikey",
264
+ helpUrl: "https://makersuite.google.com/app/apikey"
265
+ }
266
+ },
267
+ icon: "💎"
268
+ },
269
+ atlascloud: {
270
+ name: "Atlas Cloud",
271
+ description: "Multi-provider API with OpenAI, Anthropic, and more",
272
+ authChoice: "atlascloud-api-key",
273
+ placeholder: "aat_...",
274
+ fields: {
275
+ authSecret: {
276
+ label: "API Key",
277
+ type: "password",
278
+ help: "Get your key from your Atlas Cloud dashboard",
279
+ helpUrl: "https://atlascloud.ai"
280
+ },
281
+ baseUrl: {
282
+ label: "Base URL (optional)",
283
+ type: "text",
284
+ placeholder: "https://api.atlascloud.ai/v1",
285
+ default: "https://api.atlascloud.ai/v1"
286
+ }
287
+ },
288
+ icon: "☁️"
289
+ },
290
+ openrouter: {
291
+ name: "OpenRouter",
292
+ description: "Access to 100+ AI models through one API",
293
+ authChoice: "openrouter-api-key",
294
+ placeholder: "sk-or-...",
295
+ fields: {
296
+ authSecret: {
297
+ label: "API Key",
298
+ type: "password",
299
+ help: "Get your key from https://openrouter.ai/keys",
300
+ helpUrl: "https://openrouter.ai/keys"
301
+ }
302
+ },
303
+ icon: "🔀"
304
+ }
305
+ };
306
+
307
+ app.get("/setup/api/templates", requireSetupAuth, (_req, res) => {
308
+ res.json({ templates: PROVIDER_TEMPLATES });
309
+ });
310
+
311
+ app.get("/setup/api/templates/:provider", requireSetupAuth, (req, res) => {
312
+ const template = PROVIDER_TEMPLATES[req.params.provider];
313
+ if (!template) {
314
+ return res.status(404).json({ error: "Template not found" });
315
+ }
316
+ res.json(template);
317
+ });
318
+
319
+ // Pre-flight validation checks
320
+ app.get("/setup/api/check", requireSetupAuth, async (_req, res) => {
321
+ const checks = [];
322
+
323
+ // Check Moltbot CLI
324
+ try {
325
+ const versionResult = await runCmd(MOLTBOT_NODE, moltArgs(["--version"]));
326
+ checks.push({
327
+ name: "Moltbot CLI",
328
+ status: "ok",
329
+ message: `Version ${versionResult.output.trim()}`
330
+ });
331
+ } catch (err) {
332
+ checks.push({
333
+ name: "Moltbot CLI",
334
+ status: "error",
335
+ message: `CLI not accessible: ${String(err)}`
336
+ });
337
+ }
338
+
339
+ // Check state directory
340
+ try {
341
+ fs.mkdirSync(STATE_DIR, { recursive: true });
342
+ const stateAccessible = fs.accessSync(STATE_DIR, fs.constants.W_OK);
343
+ checks.push({
344
+ name: "State Directory",
345
+ status: "ok",
346
+ message: `Writable: ${STATE_DIR}`
347
+ });
348
+ } catch (err) {
349
+ checks.push({
350
+ name: "State Directory",
351
+ status: "error",
352
+ message: `Cannot write to ${STATE_DIR}: ${String(err)}`
353
+ });
354
+ }
355
+
356
+ // Check workspace directory
357
+ try {
358
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
359
+ checks.push({
360
+ name: "Workspace Directory",
361
+ status: "ok",
362
+ message: `Writable: ${WORKSPACE_DIR}`
363
+ });
364
+ } catch (err) {
365
+ checks.push({
366
+ name: "Workspace Directory",
367
+ status: "error",
368
+ message: `Cannot write to ${WORKSPACE_DIR}: ${String(err)}`
369
+ });
370
+ }
371
+
372
+ // Check memory
373
+ const totalMem = Math.round(os.totalmem() / 1024 / 1024);
374
+ const freeMem = Math.round(os.freemem() / 1024 / 1024);
375
+ checks.push({
376
+ name: "Available Memory",
377
+ status: freeMem > 256 ? "ok" : "warning",
378
+ message: `${freeMem}MB available (512MB+ recommended)`,
379
+ value: freeMem
380
+ });
381
+
382
+ // Check disk space
383
+ try {
384
+ const stats = await fs.promises.statfs(STATE_DIR);
385
+ // Validate statfs values exist and are valid numbers
386
+ if (stats && typeof stats.bavail === 'number' && typeof stats.frsize === 'number' && stats.bavail > 0 && stats.frsize > 0) {
387
+ const freeSpace = Math.round(stats.bavail * stats.frsize / 1024 / 1024);
388
+ checks.push({
389
+ name: "Disk Space",
390
+ status: freeSpace > 100 ? "ok" : "warning",
391
+ message: `${freeSpace}MB available`,
392
+ value: freeSpace
393
+ });
394
+ } else {
395
+ // statfs succeeded but returned invalid data (common in some container environments)
396
+ // Don't fail the check - Railway volumes are typically large enough
397
+ checks.push({
398
+ name: "Disk Space",
399
+ status: "ok",
400
+ message: "OK (could not measure, Railway volumes typically 1GB+)"
401
+ });
402
+ }
403
+ } catch (err) {
404
+ // statfs not available or failed (common in Windows/some containers)
405
+ // Don't fail the check - this is expected in some environments
406
+ checks.push({
407
+ name: "Disk Space",
408
+ status: "ok",
409
+ message: "OK (could not measure, Railway volumes typically 1GB+)"
410
+ });
411
+ }
412
+
413
+ const allOk = checks.every(c => c.status === "ok");
414
+ res.json({
415
+ ready: allOk,
416
+ checks,
417
+ summary: allOk ? "All checks passed. Ready to setup." : "Some checks failed. Please fix issues before proceeding."
418
+ });
419
+ });
420
+
421
+ // Token validation endpoint
422
+ app.post("/setup/api/validate-token", requireSetupAuth, async (req, res) => {
423
+ const { provider, token, baseUrl } = req.body || {};
424
+
425
+ if (!provider || !token) {
426
+ return res.status(400).json({ valid: false, error: "Provider and token are required" });
427
+ }
428
+
429
+ // Validate token format first (quick check)
430
+ let formatValid = true;
431
+ let formatError = "";
432
+
433
+ switch (provider) {
434
+ case "openai-api-key":
435
+ if (!token.startsWith("sk-")) {
436
+ formatValid = false;
437
+ formatError = "OpenAI API keys should start with 'sk-'";
438
+ }
439
+ break;
440
+ case "anthropic-api-key":
441
+ if (!token.startsWith("sk-ant-")) {
442
+ formatValid = false;
443
+ formatError = "Anthropic API keys should start with 'sk-ant-'";
444
+ }
445
+ break;
446
+ case "gemini-api-key":
447
+ if (!token.startsWith("AIza")) {
448
+ formatValid = false;
449
+ formatError = "Gemini API keys should start with 'AIza'";
450
+ }
451
+ break;
452
+ case "atlascloud-api-key":
453
+ if (!token.startsWith("aat_")) {
454
+ formatValid = false;
455
+ formatError = "Atlas Cloud API keys should start with 'aat_'";
456
+ }
457
+ break;
458
+ case "openrouter-api-key":
459
+ if (!token.startsWith("sk-or-")) {
460
+ formatValid = false;
461
+ formatError = "OpenRouter API keys should start with 'sk-or-'";
462
+ }
463
+ break;
464
+ }
465
+
466
+ if (!formatValid) {
467
+ return res.json({ valid: false, error: formatError });
468
+ }
469
+
470
+ // Quick validation by attempting to use the token
471
+ try {
472
+ let validationOk = false;
473
+ let providerName = "";
474
+
475
+ if (provider === "openai-api-key") {
476
+ // Test with a minimal API call
477
+ const response = await fetch("https://api.openai.com/v1/models", {
478
+ headers: { "Authorization": `Bearer ${token}` },
479
+ signal: AbortSignal.timeout(5000)
480
+ });
481
+ validationOk = response.ok;
482
+ providerName = "OpenAI";
483
+ } else if (provider === "anthropic-api-key") {
484
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
485
+ method: "POST",
486
+ headers: {
487
+ "x-api-key": token,
488
+ "anthropic-version": "2023-06-01",
489
+ "content-type": "application/json"
490
+ },
491
+ body: JSON.stringify({
492
+ model: "claude-3-haiku-20240307",
493
+ max_tokens: 1,
494
+ messages: [{ role: "user", content: "test" }]
495
+ }),
496
+ signal: AbortSignal.timeout(5000)
497
+ });
498
+ validationOk = response.status !== 401;
499
+ providerName = "Anthropic";
500
+ } else if (provider === "atlascloud-api-key") {
501
+ const apiUrl = baseUrl || "https://api.atlascloud.ai/v1";
502
+ const response = await fetch(`${apiUrl}/models`, {
503
+ headers: { "Authorization": `Bearer ${token}` },
504
+ signal: AbortSignal.timeout(5000)
505
+ });
506
+ validationOk = response.ok;
507
+ providerName = "Atlas Cloud";
508
+ } else {
509
+ // For other providers, just verify format
510
+ validationOk = true;
511
+ }
512
+
513
+ res.json({
514
+ valid: validationOk,
515
+ provider: providerName,
516
+ message: validationOk ? "Token validated successfully" : "Token validation failed"
517
+ });
518
+ } catch (err) {
519
+ res.json({
520
+ valid: false,
521
+ error: `Validation failed: ${err.message}`
522
+ });
523
+ }
524
+ });
525
+
526
+ // SSE endpoint for streaming setup progress
527
+ app.get("/setup/api/progress", requireSetupAuth, async (req, res) => {
528
+ res.setHeader("Content-Type", "text/event-stream");
529
+ res.setHeader("Cache-Control", "no-cache");
530
+ res.setHeader("Connection", "keep-alive");
531
+
532
+ const sendEvent = (event, data) => {
533
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
534
+ };
535
+
536
+ try {
537
+ // Check if already configured
538
+ if (isConfigured()) {
539
+ sendEvent("progress", { stage: "complete", message: "Already configured" });
540
+ await ensureGatewayRunning();
541
+ sendEvent("done", { success: true, message: "Setup already complete" });
542
+ return res.end();
543
+ }
544
+
545
+ sendEvent("progress", { stage: "starting", message: "Initializing setup..." });
546
+
547
+ // Create directories
548
+ fs.mkdirSync(STATE_DIR, { recursive: true });
549
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
550
+ sendEvent("progress", { stage: "directories", message: "Created state directories" });
551
+
552
+ // Get the setup payload from query or session
553
+ // For simplicity, we'll expect the client to POST to run first, then poll for progress
554
+ sendEvent("progress", { stage: "waiting", message: "Waiting for setup parameters..." });
555
+
556
+ } catch (err) {
557
+ sendEvent("error", { error: String(err) });
558
+ res.end();
559
+ }
560
+ });
561
+
562
+ app.get("/setup/app.js", requireSetupAuth, (_req, res) => {
563
+ // Serve JS for /setup (kept external to avoid inline encoding/template issues)
564
+ res.type("application/javascript");
565
+ res.send(fs.readFileSync(path.join(process.cwd(), "src", "setup-app.js"), "utf8"));
566
+ });
567
+
568
+ app.get("/setup", requireSetupAuth, (_req, res) => {
569
+ // No inline <script>: serve JS from /setup/app.js to avoid any encoding/template-literal issues.
570
+ res.type("html").send(`<!doctype html>
571
+ <html>
572
+ <head>
573
+ <meta charset="utf-8" />
574
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
575
+ <title>Moltbot Setup</title>
576
+ <style>
577
+ body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; margin: 2rem; max-width: 900px; }
578
+ .card { border: 1px solid #ddd; border-radius: 12px; padding: 1.25rem; margin: 1rem 0; }
579
+ label { display:block; margin-top: 0.75rem; font-weight: 600; }
580
+ input, select { width: 100%; padding: 0.6rem; margin-top: 0.25rem; }
581
+ button { padding: 0.8rem 1.2rem; border-radius: 10px; border: 0; background: #111; color: #fff; font-weight: 700; cursor: pointer; }
582
+ code { background: #f6f6f6; padding: 0.1rem 0.3rem; border-radius: 6px; }
583
+ .muted { color: #555; }
584
+ </style>
585
+ </head>
586
+ <body>
587
+ <h1>Moltbot Setup</h1>
588
+ <p class="muted">This wizard configures Moltbot by running the same onboarding command it uses in the terminal, but from the browser.</p>
589
+
590
+ <div class="card">
591
+ <h2>Status</h2>
592
+ <div id="status">Loading...</div>
593
+ <div style="margin-top: 0.75rem">
594
+ <a href="/moltbot" target="_blank">Open Moltbot UI</a>
595
+ &nbsp;|&nbsp;
596
+ <a href="/setup/export" target="_blank">Download backup (.tar.gz)</a>
597
+ </div>
598
+ </div>
599
+
600
+ <div class="card">
601
+ <h2>🚀 Quick Setup</h2>
602
+ <p class="muted">Click a provider to auto-fill configuration:</p>
603
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; margin: 1rem 0;">
604
+ <button onclick="useProvider('openai')" style="background:#10a37f; padding:0.6rem;">🤖 OpenAI</button>
605
+ <button onclick="useProvider('anthropic')" style="background:#d4a573; color:white; padding:0.6rem;">🧠 Anthropic</button>
606
+ <button onclick="useProvider('google')" style="background:#4285f4; color:white; padding:0.6rem;">💎 Gemini</button>
607
+ <button onclick="useProvider('atlascloud')" style="background:#8b5cf6; color:white; padding:0.6rem;">☁️ Atlas Cloud</button>
608
+ <button onclick="useProvider('openrouter')" style="background:#6366f1; color:white; padding:0.6rem;">🔀 OpenRouter</button>
609
+ </div>
610
+ </div>
611
+
612
+ <div class="card">
613
+ <h2>✅ Pre-flight Checks</h2>
614
+ <button onclick="runPreflightChecks()" style="margin-bottom: 0.75rem;">Run Checks</button>
615
+ <div id="preflightResults" style="margin-top: 0.75rem;"></div>
616
+ </div>
617
+
618
+ <div class="card">
619
+ <h2>1) Model/auth provider</h2>
620
+ <p class="muted">Matches the groups shown in the terminal onboarding.</p>
621
+ <label>Provider group</label>
622
+ <select id="authGroup"></select>
623
+
624
+ <label>Auth method</label>
625
+ <select id="authChoice"></select>
626
+
627
+ <label>Key / Token (if required)</label>
628
+ <input id="authSecret" type="password" placeholder="Paste API key / token if applicable" onblur="validateToken()" />
629
+ <button id="validateBtn" onclick="validateToken()" type="button" style="margin-top:0.5rem; padding:0.5rem 1rem; background:#6b7280; font-size:0.9rem;">Validate Token</button>
630
+ <div id="authSecretHelp" class="muted" style="margin-top: 0.5rem; display:none;"></div>
631
+
632
+ <label>Base URL (optional, for Atlas Cloud)</label>
633
+ <input id="baseUrl" type="text" placeholder="https://api.atlascloud.ai/v1" />
634
+ <div class="muted" style="margin-top: 0.25rem;">Leave blank to use provider default</div>
635
+
636
+ <label>Wizard flow</label>
637
+ <select id="flow">
638
+ <option value="quickstart">quickstart</option>
639
+ <option value="advanced">advanced</option>
640
+ <option value="manual">manual</option>
641
+ </select>
642
+ </div>
643
+
644
+ <div class="card">
645
+ <h2>2) Optional: Channels</h2>
646
+ <p class="muted">You can also add channels later inside Moltbot, but this helps you get messaging working immediately.</p>
647
+
648
+ <label>Telegram bot token (optional)</label>
649
+ <input id="telegramToken" type="password" placeholder="123456:ABC..." />
650
+ <div class="muted" style="margin-top: 0.25rem">
651
+ Get it from BotFather: open Telegram, message <code>@BotFather</code>, run <code>/newbot</code>, then copy the token.
652
+ </div>
653
+
654
+ <label>Discord bot token (optional)</label>
655
+ <input id="discordToken" type="password" placeholder="Bot token" />
656
+ <div class="muted" style="margin-top: 0.25rem">
657
+ Get it from the Discord Developer Portal: create an application, add a Bot, then copy the Bot Token.<br/>
658
+ <strong>Important:</strong> Enable <strong>MESSAGE CONTENT INTENT</strong> in Bot → Privileged Gateway Intents, or the bot will crash on startup.
659
+ </div>
660
+
661
+ <label>Slack bot token (optional)</label>
662
+ <input id="slackBotToken" type="password" placeholder="xoxb-..." />
663
+
664
+ <label>Slack app token (optional)</label>
665
+ <input id="slackAppToken" type="password" placeholder="xapp-..." />
666
+ </div>
667
+
668
+ <div class="card">
669
+ <h2>3) Run onboarding</h2>
670
+ <button id="run">Run setup</button>
671
+ <button id="pairingApprove" style="background:#1f2937; margin-left:0.5rem">Approve pairing</button>
672
+ <button id="reset" style="background:#444; margin-left:0.5rem">Reset setup</button>
673
+ <button id="debug" style="background:#1f2937; margin-left:0.5rem">Debug info</button>
674
+ <button id="test" style="background:#059669; margin-left:0.5rem">Test endpoint</button>
675
+ <pre id="log" style="white-space:pre-wrap"></pre>
676
+ <p class="muted">Reset deletes the Moltbot config file so you can rerun onboarding. Pairing approval lets you grant DM access when dmPolicy=pairing. Debug info shows current system state. Test endpoint verifies routing works.</p>
677
+ </div>
678
+
679
+ <script src="/setup/app.js"></script>
680
+ </body>
681
+ </html>`);
682
+ });
683
+
684
+ app.get("/setup/api/status", requireSetupAuth, async (_req, res) => {
685
+ const version = await runCmd(MOLTBOT_NODE, moltArgs(["--version"]));
686
+ const channelsHelp = await runCmd(MOLTBOT_NODE, moltArgs(["channels", "add", "--help"]));
687
+
688
+ // We reuse Moltbot's own auth-choice grouping logic indirectly by hardcoding the same group defs.
689
+ // This is intentionally minimal; later we can parse the CLI help output to stay perfectly in sync.
690
+ const authGroups = [
691
+ { value: "openai", label: "OpenAI", hint: "Codex OAuth + API key", options: [
692
+ { value: "codex-cli", label: "OpenAI Codex OAuth (Codex CLI)" },
693
+ { value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
694
+ { value: "openai-api-key", label: "OpenAI API key" }
695
+ ]},
696
+ { value: "anthropic", label: "Anthropic", hint: "Claude Code CLI + API key", options: [
697
+ { value: "claude-cli", label: "Anthropic token (Claude Code CLI)" },
698
+ { value: "token", label: "Anthropic token (paste setup-token)" },
699
+ { value: "apiKey", label: "Anthropic API key" }
700
+ ]},
701
+ { value: "google", label: "Google", hint: "Gemini API key + OAuth", options: [
702
+ { value: "gemini-api-key", label: "Google Gemini API key" },
703
+ { value: "google-antigravity", label: "Google Antigravity OAuth" },
704
+ { value: "google-gemini-cli", label: "Google Gemini CLI OAuth" }
705
+ ]},
706
+ { value: "openrouter", label: "OpenRouter", hint: "API key", options: [
707
+ { value: "openrouter-api-key", label: "OpenRouter API key" }
708
+ ]},
709
+ { value: "atlascloud", label: "Atlas Cloud", hint: "Multi-provider API", options: [
710
+ { value: "atlascloud-api-key", label: "Atlas Cloud API key" }
711
+ ]},
712
+ { value: "ai-gateway", label: "Vercel AI Gateway", hint: "API key", options: [
713
+ { value: "ai-gateway-api-key", label: "Vercel AI Gateway API key" }
714
+ ]},
715
+ { value: "moonshot", label: "Moonshot AI", hint: "Kimi K2 + Kimi Code", options: [
716
+ { value: "moonshot-api-key", label: "Moonshot AI API key" },
717
+ { value: "kimi-code-api-key", label: "Kimi Code API key" }
718
+ ]},
719
+ { value: "zai", label: "Z.AI (GLM 4.7)", hint: "API key", options: [
720
+ { value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" }
721
+ ]},
722
+ { value: "minimax", label: "MiniMax", hint: "M2.1 (recommended)", options: [
723
+ { value: "minimax-api", label: "MiniMax M2.1" },
724
+ { value: "minimax-api-lightning", label: "MiniMax M2.1 Lightning" }
725
+ ]},
726
+ { value: "qwen", label: "Qwen", hint: "OAuth", options: [
727
+ { value: "qwen-portal", label: "Qwen OAuth" }
728
+ ]},
729
+ { value: "copilot", label: "Copilot", hint: "GitHub + local proxy", options: [
730
+ { value: "github-copilot", label: "GitHub Copilot (GitHub device login)" },
731
+ { value: "copilot-proxy", label: "Copilot Proxy (local)" }
732
+ ]},
733
+ { value: "synthetic", label: "Synthetic", hint: "Anthropic-compatible (multi-model)", options: [
734
+ { value: "synthetic-api-key", label: "Synthetic API key" }
735
+ ]},
736
+ { value: "opencode-zen", label: "OpenCode Zen", hint: "API key", options: [
737
+ { value: "opencode-zen", label: "OpenCode Zen (multi-model proxy)" }
738
+ ]}
739
+ ];
740
+
741
+ res.json({
742
+ configured: isConfigured(),
743
+ gatewayTarget: GATEWAY_TARGET,
744
+ moltbotVersion: version.output.trim(),
745
+ channelsAddHelp: channelsHelp.output,
746
+ authGroups,
747
+ });
748
+ });
749
+
750
+ function buildOnboardArgs(payload) {
751
+ const args = [
752
+ "onboard",
753
+ "--non-interactive",
754
+ "--accept-risk",
755
+ "--json",
756
+ "--no-install-daemon",
757
+ "--skip-health",
758
+ "--workspace",
759
+ WORKSPACE_DIR,
760
+ // The wrapper owns public networking; keep the gateway internal.
761
+ "--gateway-bind",
762
+ "loopback",
763
+ "--gateway-port",
764
+ String(INTERNAL_GATEWAY_PORT),
765
+ "--gateway-auth",
766
+ "token",
767
+ "--gateway-token",
768
+ MOLTBOT_GATEWAY_TOKEN,
769
+ "--flow",
770
+ payload.flow || "quickstart"
771
+ ];
772
+
773
+ if (payload.authChoice) {
774
+ args.push("--auth-choice", payload.authChoice);
775
+
776
+ // Map secret to correct flag for common choices.
777
+ const secret = (payload.authSecret || "").trim();
778
+ const map = {
779
+ "openai-api-key": "--openai-api-key",
780
+ "apiKey": "--anthropic-api-key",
781
+ "atlascloud-api-key": "--atlascloud-api-key",
782
+ "openrouter-api-key": "--openrouter-api-key",
783
+ "ai-gateway-api-key": "--ai-gateway-api-key",
784
+ "moonshot-api-key": "--moonshot-api-key",
785
+ "kimi-code-api-key": "--kimi-code-api-key",
786
+ "gemini-api-key": "--gemini-api-key",
787
+ "zai-api-key": "--zai-api-key",
788
+ "minimax-api": "--minimax-api-key",
789
+ "minimax-api-lightning": "--minimax-api-key",
790
+ "synthetic-api-key": "--synthetic-api-key",
791
+ "opencode-zen": "--opencode-zen-api-key"
792
+ };
793
+ const flag = map[payload.authChoice];
794
+ if (flag && secret) {
795
+ args.push(flag, secret);
796
+ }
797
+
798
+ if (payload.authChoice === "token" && secret) {
799
+ // This is the Anthropics setup-token flow.
800
+ args.push("--token-provider", "anthropic", "--token", secret);
801
+ }
802
+ }
803
+
804
+ return args;
805
+ }
806
+
807
+ function runCmd(cmd, args, opts = {}) {
808
+ return new Promise((resolve) => {
809
+ const proc = childProcess.spawn(cmd, args, {
810
+ ...opts,
811
+ env: {
812
+ ...process.env,
813
+ CLAWDBOT_STATE_DIR: STATE_DIR,
814
+ CLAWDBOT_WORKSPACE_DIR: WORKSPACE_DIR,
815
+ },
816
+ });
817
+
818
+ let out = "";
819
+ proc.stdout?.on("data", (d) => (out += d.toString("utf8")));
820
+ proc.stderr?.on("data", (d) => (out += d.toString("utf8")));
821
+
822
+ proc.on("error", (err) => {
823
+ out += `\n[spawn error] ${String(err)}\n`;
824
+ resolve({ code: 127, output: out });
825
+ });
826
+
827
+ proc.on("close", (code) => resolve({ code: code ?? 0, output: out }));
828
+ });
829
+ }
830
+
831
+ app.post("/setup/api/run", requireSetupAuth, async (req, res) => {
832
+ try {
833
+ if (isConfigured()) {
834
+ await ensureGatewayRunning();
835
+ return res.json({ ok: true, output: "Already configured.\nUse Reset setup if you want to rerun onboarding.\n" });
836
+ }
837
+
838
+ fs.mkdirSync(STATE_DIR, { recursive: true });
839
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
840
+
841
+ const payload = req.body || {};
842
+ const onboardArgs = buildOnboardArgs(payload);
843
+
844
+ // Debug logging
845
+ console.error('[DEBUG] Onboard args:', onboardArgs);
846
+ console.error('[DEBUG] Payload authChoice:', payload.authChoice);
847
+ console.error('[DEBUG] Payload authSecret length:', payload.authSecret?.length || 0);
848
+ console.error('[DEBUG] Config path:', configPath());
849
+
850
+ const onboard = await runCmd(MOLTBOT_NODE, moltArgs(onboardArgs));
851
+
852
+ console.error('[DEBUG] Onboard exit code:', onboard.code);
853
+ console.error('[DEBUG] isConfigured() after onboard:', isConfigured());
854
+ console.error('[DEBUG] Config file exists:', fs.existsSync(configPath()));
855
+
856
+ let extra = "";
857
+
858
+ const ok = onboard.code === 0 && isConfigured();
859
+
860
+ console.error('[DEBUG] Setup ok status:', ok, '(exit code:', onboard.code, ', configured:', isConfigured(), ')');
861
+
862
+ // Optional channel setup (only after successful onboarding, and only if the installed CLI supports it).
863
+ if (ok) {
864
+ console.error('[DEBUG] Configuring gateway settings...');
865
+
866
+ // Configure gateway to run in loopback mode without token authentication.
867
+ // The wrapper protects the gateway with SETUP_PASSWORD, so token auth is redundant
868
+ // and makes it difficult for the web UI to authenticate.
869
+ await runCmd(MOLTBOT_NODE, moltArgs(["config", "set", "gateway.auth.mode", "none"]));
870
+ await runCmd(MOLTBOT_NODE, moltArgs(["config", "set", "gateway.bind", "loopback"]));
871
+ await runCmd(MOLTBOT_NODE, moltArgs(["config", "set", "gateway.port", String(INTERNAL_GATEWAY_PORT)]));
872
+
873
+ console.error('[DEBUG] Gateway settings configured, restarting gateway...');
874
+
875
+ const channelsHelp = await runCmd(MOLTBOT_NODE, moltArgs(["channels", "add", "--help"]));
876
+ const helpText = channelsHelp.output || "";
877
+
878
+ const supports = (name) => helpText.includes(name);
879
+
880
+ if (payload.telegramToken?.trim()) {
881
+ if (!supports("telegram")) {
882
+ extra += "\n[telegram] skipped (this moltbot build does not list telegram in `channels add --help`)\n";
883
+ } else {
884
+ // Avoid `channels add` here (it has proven flaky across builds); write config directly.
885
+ const token = payload.telegramToken.trim();
886
+ const cfgObj = {
887
+ enabled: true,
888
+ dmPolicy: "pairing",
889
+ botToken: token,
890
+ groupPolicy: "allowlist",
891
+ streamMode: "partial",
892
+ };
893
+ const set = await runCmd(
894
+ MOLTBOT_NODE,
895
+ moltArgs(["config", "set", "--json", "channels.telegram", JSON.stringify(cfgObj)]),
896
+ );
897
+ const get = await runCmd(MOLTBOT_NODE, moltArgs(["config", "get", "channels.telegram"]));
898
+ extra += `\n[telegram config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}`;
899
+ extra += `\n[telegram verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
900
+ }
901
+ }
902
+
903
+ if (payload.discordToken?.trim()) {
904
+ if (!supports("discord")) {
905
+ extra += "\n[discord] skipped (this moltbot build does not list discord in `channels add --help`)\n";
906
+ } else {
907
+ const token = payload.discordToken.trim();
908
+ const cfgObj = {
909
+ enabled: true,
910
+ token,
911
+ groupPolicy: "allowlist",
912
+ dm: {
913
+ policy: "pairing",
914
+ },
915
+ };
916
+ const set = await runCmd(
917
+ MOLTBOT_NODE,
918
+ moltArgs(["config", "set", "--json", "channels.discord", JSON.stringify(cfgObj)]),
919
+ );
920
+ const get = await runCmd(MOLTBOT_NODE, moltArgs(["config", "get", "channels.discord"]));
921
+ extra += `\n[discord config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}`;
922
+ extra += `\n[discord verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
923
+ }
924
+ }
925
+
926
+ if (payload.slackBotToken?.trim() || payload.slackAppToken?.trim()) {
927
+ if (!supports("slack")) {
928
+ extra += "\n[slack] skipped (this moltbot build does not list slack in `channels add --help`)\n";
929
+ } else {
930
+ const cfgObj = {
931
+ enabled: true,
932
+ botToken: payload.slackBotToken?.trim() || undefined,
933
+ appToken: payload.slackAppToken?.trim() || undefined,
934
+ };
935
+ const set = await runCmd(
936
+ MOLTBOT_NODE,
937
+ moltArgs(["config", "set", "--json", "channels.slack", JSON.stringify(cfgObj)]),
938
+ );
939
+ const get = await runCmd(MOLTBOT_NODE, moltArgs(["config", "get", "channels.slack"]));
940
+ extra += `\n[slack config] exit=${set.code} (output ${set.output.length} chars)\n${set.output || "(no output)"}`;
941
+ extra += `\n[slack verify] exit=${get.code} (output ${get.output.length} chars)\n${get.output || "(no output)"}`;
942
+ }
943
+ }
944
+
945
+ // Apply changes immediately.
946
+ console.error('[DEBUG] Restarting gateway...');
947
+ await restartGateway();
948
+ console.error('[DEBUG] Gateway restarted, checking if running...');
949
+ const running = gatewayProc !== null;
950
+ console.error('[DEBUG] Gateway running after restart:', running);
951
+ }
952
+
953
+ console.error('[DEBUG] Returning response: ok=', ok);
954
+ return res.status(ok ? 200 : 500).json({
955
+ ok,
956
+ output: `${onboard.output}${extra}`,
957
+ });
958
+ } catch (err) {
959
+ console.error("[/setup/api/run] error:", err);
960
+ return res.status(500).json({ ok: false, output: `Internal error: ${String(err)}` });
961
+ }
962
+ });
963
+
964
+ app.get("/setup/api/debug", requireSetupAuth, async (_req, res) => {
965
+ const v = await runCmd(MOLTBOT_NODE, moltArgs(["--version"]));
966
+ const help = await runCmd(MOLTBOT_NODE, moltArgs(["channels", "add", "--help"]));
967
+ res.json({
968
+ wrapper: {
969
+ node: process.version,
970
+ port: PORT,
971
+ stateDir: STATE_DIR,
972
+ workspaceDir: WORKSPACE_DIR,
973
+ configPath: configPath(),
974
+ gatewayTokenFromEnv: Boolean(
975
+ process.env.MOLTBOT_GATEWAY_TOKEN?.trim() ||
976
+ process.env.CLAWDBOT_GATEWAY_TOKEN?.trim()
977
+ ),
978
+ gatewayTokenPersisted: fs.existsSync(path.join(STATE_DIR, "gateway.token")),
979
+ railwayCommit: process.env.RAILWAY_GIT_COMMIT_SHA || null,
980
+ },
981
+ moltbot: {
982
+ entry: MOLTBOT_ENTRY,
983
+ node: MOLTBOT_NODE,
984
+ version: v.output.trim(),
985
+ channelsAddHelpIncludesTelegram: help.output.includes("telegram"),
986
+ },
987
+ });
988
+ });
989
+
990
+ app.post("/setup/api/pairing/approve", requireSetupAuth, async (req, res) => {
991
+ const { channel, code } = req.body || {};
992
+ if (!channel || !code) {
993
+ return res.status(400).json({ ok: false, error: "Missing channel or code" });
994
+ }
995
+ const r = await runCmd(MOLTBOT_NODE, moltArgs(["pairing", "approve", String(channel), String(code)]));
996
+ return res.status(r.code === 0 ? 200 : 500).json({ ok: r.code === 0, output: r.output });
997
+ });
998
+
999
+ // Simple test endpoint to verify routing works
1000
+ app.get("/setup/api/test", requireSetupAuth, (_req, res) => {
1001
+ console.error('[TEST] Test endpoint called');
1002
+ res.json({ test: "ok", message: "Routing works!" });
1003
+ });
1004
+
1005
+ // Debug endpoint to view config and gateway status
1006
+ app.get("/setup/api/debug", requireSetupAuth, async (_req, res) => {
1007
+ try {
1008
+ console.error('[DEBUG] Building debug info...');
1009
+
1010
+ const cfgPath = configPath();
1011
+ console.error('[DEBUG] configPath():', cfgPath);
1012
+
1013
+ const cfgExists = fs.existsSync(cfgPath);
1014
+ console.error('[DEBUG] config exists:', cfgExists);
1015
+
1016
+ const stateExists = fs.existsSync(STATE_DIR);
1017
+ console.error('[DEBUG] state dir exists:', stateExists);
1018
+
1019
+ const workspaceExists = fs.existsSync(WORKSPACE_DIR);
1020
+ console.error('[DEBUG] workspace dir exists:', workspaceExists);
1021
+
1022
+ const configured = isConfigured();
1023
+ console.error('[DEBUG] isConfigured():', configured);
1024
+
1025
+ const info = {
1026
+ configPath: cfgPath,
1027
+ configExists: cfgExists,
1028
+ stateDir: STATE_DIR,
1029
+ stateDirExists: stateExists,
1030
+ workspaceDir: WORKSPACE_DIR,
1031
+ workspaceDirExists: workspaceExists,
1032
+ gatewayRunning: gatewayProc !== null,
1033
+ gatewayProcExists: gatewayProc !== null,
1034
+ gatewayProcPid: gatewayProc?.pid || null,
1035
+ configured: configured,
1036
+ };
1037
+
1038
+ console.error('[DEBUG] Built info object:', JSON.stringify(info));
1039
+
1040
+ if (info.configExists) {
1041
+ try {
1042
+ info.configContent = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
1043
+ console.error('[DEBUG] Config content loaded');
1044
+ } catch (e) {
1045
+ info.configError = String(e);
1046
+ console.error('[DEBUG] Config error:', e);
1047
+ }
1048
+ }
1049
+
1050
+ console.error('[DEBUG] Sending response');
1051
+ res.json(info);
1052
+ } catch (err) {
1053
+ console.error('[DEBUG] Error in debug endpoint:', err);
1054
+ res.status(500).json({ error: String(err), stack: err.stack });
1055
+ }
1056
+ });
1057
+
1058
+ app.post("/setup/api/reset", requireSetupAuth, async (_req, res) => {
1059
+ // Minimal reset: delete the config file so /setup can rerun.
1060
+ // Keep credentials/sessions/workspace by default.
1061
+ try {
1062
+ fs.rmSync(configPath(), { force: true });
1063
+ res.type("text/plain").send("OK - deleted config file. You can rerun setup now.");
1064
+ } catch (err) {
1065
+ res.status(500).type("text/plain").send(String(err));
1066
+ }
1067
+ });
1068
+
1069
+ app.get("/setup/export", requireSetupAuth, async (_req, res) => {
1070
+ fs.mkdirSync(STATE_DIR, { recursive: true });
1071
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
1072
+
1073
+ res.setHeader("content-type", "application/gzip");
1074
+ res.setHeader(
1075
+ "content-disposition",
1076
+ `attachment; filename="moltbot-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.tar.gz"`,
1077
+ );
1078
+
1079
+ // Prefer exporting from a common /data root so archives are easy to inspect and restore.
1080
+ // This preserves dotfiles like /data/.moltbot/moltbot.json.
1081
+ const stateAbs = path.resolve(STATE_DIR);
1082
+ const workspaceAbs = path.resolve(WORKSPACE_DIR);
1083
+
1084
+ const dataRoot = "/data";
1085
+ const underData = (p) => p === dataRoot || p.startsWith(dataRoot + path.sep);
1086
+
1087
+ let cwd = "/";
1088
+ let paths = [stateAbs, workspaceAbs].map((p) => p.replace(/^\//, ""));
1089
+
1090
+ if (underData(stateAbs) && underData(workspaceAbs)) {
1091
+ cwd = dataRoot;
1092
+ // We export relative to /data so the archive contains: .moltbot/... and workspace/...
1093
+ paths = [
1094
+ path.relative(dataRoot, stateAbs) || ".",
1095
+ path.relative(dataRoot, workspaceAbs) || ".",
1096
+ ];
1097
+ }
1098
+
1099
+ const stream = tar.c(
1100
+ {
1101
+ gzip: true,
1102
+ portable: true,
1103
+ noMtime: true,
1104
+ cwd,
1105
+ onwarn: () => {},
1106
+ },
1107
+ paths,
1108
+ );
1109
+
1110
+ stream.on("error", (err) => {
1111
+ console.error("[export]", err);
1112
+ if (!res.headersSent) res.status(500);
1113
+ res.end(String(err));
1114
+ });
1115
+
1116
+ stream.pipe(res);
1117
+ });
1118
+
1119
+ // Proxy everything else to the gateway.
1120
+ const proxy = httpProxy.createProxyServer({
1121
+ target: GATEWAY_TARGET,
1122
+ ws: true,
1123
+ xfwd: true,
1124
+ });
1125
+
1126
+ proxy.on("error", (err, _req, _res) => {
1127
+ console.error("[proxy]", err);
1128
+ });
1129
+
1130
+ app.use(async (req, res) => {
1131
+ // If not configured, force users to /setup for any non-setup routes.
1132
+ if (!isConfigured() && !req.path.startsWith("/setup")) {
1133
+ return res.redirect("/setup");
1134
+ }
1135
+
1136
+ if (isConfigured()) {
1137
+ try {
1138
+ await ensureGatewayRunning();
1139
+ } catch (err) {
1140
+ return res.status(503).type("text/plain").send(`Gateway not ready: ${String(err)}`);
1141
+ }
1142
+ }
1143
+
1144
+ return proxy.web(req, res, { target: GATEWAY_TARGET });
1145
+ });
1146
+
1147
+ const server = app.listen(PORT, "0.0.0.0", () => {
1148
+ console.log(`[wrapper] listening on :${PORT}`);
1149
+ console.log(`[wrapper] state dir: ${STATE_DIR}`);
1150
+ console.log(`[wrapper] workspace dir: ${WORKSPACE_DIR}`);
1151
+ console.log(`[wrapper] gateway token: ${MOLTBOT_GATEWAY_TOKEN ? "(set)" : "(missing)"}`);
1152
+ console.log(`[wrapper] gateway target: ${GATEWAY_TARGET}`);
1153
+ if (!SETUP_PASSWORD) {
1154
+ console.warn("[wrapper] WARNING: SETUP_PASSWORD is not set; /setup will error.");
1155
+ }
1156
+ // Don't start gateway unless configured; proxy will ensure it starts.
1157
+ });
1158
+
1159
+ server.on("upgrade", async (req, socket, head) => {
1160
+ if (!isConfigured()) {
1161
+ socket.destroy();
1162
+ return;
1163
+ }
1164
+ try {
1165
+ await ensureGatewayRunning();
1166
+ } catch {
1167
+ socket.destroy();
1168
+ return;
1169
+ }
1170
+ proxy.ws(req, socket, head, { target: GATEWAY_TARGET });
1171
+ });
1172
+
1173
+ process.on("SIGTERM", () => {
1174
+ // Best-effort shutdown
1175
+ try {
1176
+ if (gatewayProc) gatewayProc.kill("SIGTERM");
1177
+ } catch {
1178
+ // ignore
1179
+ }
1180
+ process.exit(0);
1181
+ });
src/setup-app.js ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Served at /setup/app.js
2
+ // No fancy syntax: keep it maximally compatible.
3
+
4
+ (function () {
5
+ var statusEl = document.getElementById('status');
6
+ var authGroupEl = document.getElementById('authGroup');
7
+ var authChoiceEl = document.getElementById('authChoice');
8
+ var logEl = document.getElementById('log');
9
+
10
+ function setStatus(s) {
11
+ statusEl.textContent = s;
12
+ }
13
+
14
+ // Provider template handling
15
+ window.useProvider = function(providerId) {
16
+ httpJson('/setup/api/templates/' + providerId)
17
+ .then(function(tmpl) {
18
+ // Set auth choice
19
+ if (tmpl.authChoice) {
20
+ authChoiceEl.value = tmpl.authChoice;
21
+ // Trigger change to update options if needed
22
+ if (authGroupEl) {
23
+ authGroupEl.value = tmpl.authChoice.split('-')[0] || tmpl.authChoice;
24
+ authGroupEl.dispatchEvent(new Event('change'));
25
+ }
26
+ }
27
+
28
+ // Set placeholders and help text
29
+ var authSecretEl = document.getElementById('authSecret');
30
+ if (authSecretEl && tmpl.fields.authSecret) {
31
+ authSecretEl.placeholder = tmpl.fields.authSecret.placeholder || '';
32
+ var helpEl = document.getElementById('authSecretHelp');
33
+ if (helpEl) {
34
+ helpEl.textContent = tmpl.fields.authSecret.help || '';
35
+ helpEl.style.display = 'block';
36
+ }
37
+ }
38
+
39
+ // Set base URL if provided
40
+ if (tmpl.fields.baseUrl && tmpl.fields.baseUrl.default) {
41
+ var baseUrlEl = document.getElementById('baseUrl');
42
+ if (baseUrlEl) {
43
+ baseUrlEl.value = tmpl.fields.baseUrl.default;
44
+ }
45
+ }
46
+
47
+ logEl.textContent += '\nSelected provider: ' + tmpl.name + '\n';
48
+ })
49
+ .catch(function(err) {
50
+ logEl.textContent += '\nError loading template: ' + String(err) + '\n';
51
+ });
52
+ };
53
+
54
+ // Validate token on blur
55
+ window.validateToken = function() {
56
+ var authChoice = authChoiceEl.value;
57
+ var token = document.getElementById('authSecret').value.trim();
58
+ var baseUrl = document.getElementById('baseUrl') ? document.getElementById('baseUrl').value.trim() : '';
59
+
60
+ if (!authChoice || !token) {
61
+ return;
62
+ }
63
+
64
+ var validateBtn = document.getElementById('validateBtn');
65
+ if (validateBtn) {
66
+ validateBtn.textContent = 'Validating...';
67
+ validateBtn.disabled = true;
68
+ }
69
+
70
+ httpJson('/setup/api/validate-token', {
71
+ method: 'POST',
72
+ body: JSON.stringify({ provider: authChoice, token: token, baseUrl: baseUrl })
73
+ }).then(function(result) {
74
+ if (validateBtn) {
75
+ validateBtn.textContent = result.valid ? '✓ Valid' : '✗ Invalid';
76
+ validateBtn.disabled = false;
77
+ validateBtn.style.backgroundColor = result.valid ? '#28a745' : '#dc3545';
78
+ }
79
+
80
+ var msg = result.valid ? 'Token validated successfully for ' + result.provider : 'Validation failed: ' + result.error;
81
+ logEl.textContent += '\n' + msg + '\n';
82
+ }).catch(function(err) {
83
+ if (validateBtn) {
84
+ validateBtn.textContent = 'Validate Token';
85
+ validateBtn.disabled = false;
86
+ }
87
+ logEl.textContent += '\nValidation error: ' + String(err) + '\n';
88
+ });
89
+ };
90
+
91
+ // Pre-flight checks
92
+ window.runPreflightChecks = function() {
93
+ var resultsEl = document.getElementById('preflightResults');
94
+ resultsEl.innerHTML = 'Running checks...';
95
+ resultsEl.style.color = '#555';
96
+
97
+ httpJson('/setup/api/check')
98
+ .then(function(data) {
99
+ var html = '<div style="margin-top:0.75rem">';
100
+ html += '<strong>' + data.summary + '</strong><br><br>';
101
+
102
+ data.checks.forEach(function(check) {
103
+ var icon = check.status === 'ok' ? '✅' : check.status === 'warning' ? '⚠️' : '❌';
104
+ var color = check.status === 'ok' ? '#28a745' : check.status === 'warning' ? '#ffc107' : '#dc3545';
105
+ html += '<div style="margin:0.25rem 0; color:' + color + '">';
106
+ html += icon + ' <strong>' + check.name + ':</strong> ' + check.message;
107
+ html += '</div>';
108
+ });
109
+
110
+ html += '</div>';
111
+ resultsEl.innerHTML = html;
112
+ })
113
+ .catch(function(err) {
114
+ resultsEl.innerHTML = '<div style="color:#dc3545; margin-top:0.75rem;">Error: ' + String(err) + '</div>';
115
+ });
116
+ };
117
+
118
+ function renderAuth(groups) {
119
+ authGroupEl.innerHTML = '';
120
+ for (var i = 0; i < groups.length; i++) {
121
+ var g = groups[i];
122
+ var opt = document.createElement('option');
123
+ opt.value = g.value;
124
+ opt.textContent = g.label + (g.hint ? ' - ' + g.hint : '');
125
+ authGroupEl.appendChild(opt);
126
+ }
127
+
128
+ authGroupEl.onchange = function () {
129
+ var sel = null;
130
+ for (var j = 0; j < groups.length; j++) {
131
+ if (groups[j].value === authGroupEl.value) sel = groups[j];
132
+ }
133
+ authChoiceEl.innerHTML = '';
134
+ var opts = (sel && sel.options) ? sel.options : [];
135
+ for (var k = 0; k < opts.length; k++) {
136
+ var o = opts[k];
137
+ var opt2 = document.createElement('option');
138
+ opt2.value = o.value;
139
+ opt2.textContent = o.label + (o.hint ? ' - ' + o.hint : '');
140
+ authChoiceEl.appendChild(opt2);
141
+ }
142
+ };
143
+
144
+ authGroupEl.onchange();
145
+ }
146
+
147
+ function httpJson(url, opts) {
148
+ opts = opts || {};
149
+ opts.credentials = 'same-origin';
150
+ return fetch(url, opts).then(function (res) {
151
+ if (!res.ok) {
152
+ return res.text().then(function (t) {
153
+ throw new Error('HTTP ' + res.status + ': ' + (t || res.statusText));
154
+ });
155
+ }
156
+ return res.json();
157
+ });
158
+ }
159
+
160
+ function refreshStatus() {
161
+ setStatus('Loading...');
162
+ return httpJson('/setup/api/status').then(function (j) {
163
+ var ver = j.moltbotVersion ? (' | ' + j.moltbotVersion) : '';
164
+ setStatus((j.configured ? 'Configured - open /moltbot' : 'Not configured - run setup below') + ver);
165
+ renderAuth(j.authGroups || []);
166
+ // If channels are unsupported, surface it for debugging.
167
+ if (j.channelsAddHelp && j.channelsAddHelp.indexOf('telegram') === -1) {
168
+ logEl.textContent += '\nNote: this moltbot build does not list telegram in `channels add --help`. Telegram auto-add will be skipped.\n';
169
+ }
170
+
171
+ }).catch(function (e) {
172
+ setStatus('Error: ' + String(e));
173
+ });
174
+ }
175
+
176
+ document.getElementById('run').onclick = function () {
177
+ var payload = {
178
+ flow: document.getElementById('flow').value,
179
+ authChoice: authChoiceEl.value,
180
+ authSecret: document.getElementById('authSecret').value,
181
+ telegramToken: document.getElementById('telegramToken').value,
182
+ discordToken: document.getElementById('discordToken').value,
183
+ slackBotToken: document.getElementById('slackBotToken').value,
184
+ slackAppToken: document.getElementById('slackAppToken').value
185
+ };
186
+
187
+ logEl.textContent = 'Running...\n';
188
+
189
+ fetch('/setup/api/run', {
190
+ method: 'POST',
191
+ credentials: 'same-origin',
192
+ headers: { 'content-type': 'application/json' },
193
+ body: JSON.stringify(payload)
194
+ }).then(function (res) {
195
+ return res.text();
196
+ }).then(function (text) {
197
+ var j;
198
+ try { j = JSON.parse(text); } catch (_e) { j = { ok: false, output: text }; }
199
+ logEl.textContent += (j.output || JSON.stringify(j, null, 2));
200
+ return refreshStatus();
201
+ }).catch(function (e) {
202
+ logEl.textContent += '\nError: ' + String(e) + '\n';
203
+ });
204
+ };
205
+
206
+ // Pairing approve helper
207
+ var pairingBtn = document.getElementById('pairingApprove');
208
+ if (pairingBtn) {
209
+ pairingBtn.onclick = function () {
210
+ var channel = prompt('Enter channel (telegram or discord):');
211
+ if (!channel) return;
212
+ channel = channel.trim().toLowerCase();
213
+ if (channel !== 'telegram' && channel !== 'discord') {
214
+ alert('Channel must be "telegram" or "discord"');
215
+ return;
216
+ }
217
+ var code = prompt('Enter pairing code (e.g. 3EY4PUYS):');
218
+ if (!code) return;
219
+ logEl.textContent += '\nApproving pairing for ' + channel + '...\n';
220
+ fetch('/setup/api/pairing/approve', {
221
+ method: 'POST',
222
+ credentials: 'same-origin',
223
+ headers: { 'content-type': 'application/json' },
224
+ body: JSON.stringify({ channel: channel, code: code.trim() })
225
+ }).then(function (r) { return r.text(); })
226
+ .then(function (t) { logEl.textContent += t + '\n'; })
227
+ .catch(function (e) { logEl.textContent += 'Error: ' + String(e) + '\n'; });
228
+ };
229
+ }
230
+
231
+ document.getElementById('reset').onclick = function () {
232
+ if (!confirm('Reset setup? This deletes the config file so onboarding can run again.')) return;
233
+ logEl.textContent = 'Resetting...\n';
234
+ fetch('/setup/api/reset', { method: 'POST', credentials: 'same-origin' })
235
+ .then(function (res) { return res.text(); })
236
+ .then(function (t) { logEl.textContent += t + '\n'; return refreshStatus(); })
237
+ .catch(function (e) { logEl.textContent += 'Error: ' + String(e) + '\n'; });
238
+ };
239
+
240
+ // Debug button handler
241
+ var debugBtn = document.getElementById('debug');
242
+ if (debugBtn) {
243
+ debugBtn.onclick = function () {
244
+ logEl.textContent = 'Fetching debug info...\n';
245
+ httpJson('/setup/api/debug')
246
+ .then(function (info) {
247
+ // Check for error response
248
+ if (info.error) {
249
+ logEl.textContent = 'ERROR: ' + info.error + '\n';
250
+ if (info.stack) {
251
+ logEl.textContent += 'Stack: ' + info.stack + '\n';
252
+ }
253
+ return;
254
+ }
255
+
256
+ logEl.textContent = 'DEBUG INFO:\n';
257
+ logEl.textContent += '================\n';
258
+ logEl.textContent += 'Configured: ' + info.configured + '\n';
259
+ logEl.textContent += 'Config path: ' + info.configPath + '\n';
260
+ logEl.textContent += 'Config exists: ' + info.configExists + '\n';
261
+ logEl.textContent += 'State dir: ' + info.stateDir + ' (exists: ' + info.stateDirExists + ')\n';
262
+ logEl.textContent += 'Workspace dir: ' + info.workspaceDir + ' (exists: ' + info.workspaceDirExists + ')\n';
263
+ logEl.textContent += 'Gateway running: ' + info.gatewayRunning + '\n';
264
+ logEl.textContent += 'Gateway proc exists: ' + info.gatewayProcExists + '\n';
265
+ logEl.textContent += 'Gateway proc PID: ' + info.gatewayProcPid + '\n';
266
+ if (info.configContent) {
267
+ logEl.textContent += '\nConfig content:\n' + JSON.stringify(info.configContent, null, 2) + '\n';
268
+ } else if (info.configError) {
269
+ logEl.textContent += '\nConfig error: ' + info.configError + '\n';
270
+ }
271
+ logEl.textContent += '================\n';
272
+ })
273
+ .catch(function (e) { logEl.textContent += 'Error: ' + String(e) + '\n'; });
274
+ };
275
+ }
276
+
277
+ // Test endpoint button handler
278
+ var testBtn = document.getElementById('test');
279
+ if (testBtn) {
280
+ testBtn.onclick = function () {
281
+ logEl.textContent = 'Testing endpoint...\n';
282
+ httpJson('/setup/api/test')
283
+ .then(function (result) {
284
+ logEl.textContent = 'TEST RESULT:\n';
285
+ logEl.textContent += '============\n';
286
+ logEl.textContent += JSON.stringify(result, null, 2) + '\n';
287
+ logEl.textContent += '============\n';
288
+ if (result.test === 'ok') {
289
+ logEl.textContent += '\n✅ Routing and authentication work!\n';
290
+ }
291
+ })
292
+ .catch(function (e) { logEl.textContent += 'Error: ' + String(e) + '\n'; });
293
+ };
294
+ }
295
+
296
+ refreshStatus();
297
+
298
+ // Run pre-flight checks on load
299
+ setTimeout(function() {
300
+ runPreflightChecks();
301
+ }, 500);
302
+ })();