@aion0/forge 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,44 +1,43 @@
1
1
  ## Project: Forge (@aion0/forge)
2
2
 
3
- ### Dev Commands
3
+ ### Scripts
4
4
  ```bash
5
- # Development (hot-reload)
6
- pnpm dev
7
-
8
- # Production (local)
9
- pnpm build && pnpm start
10
-
11
- # Publish to npm (bump version in package.json first)
12
- npm login
13
- npm publish --access public --otp=<code>
14
-
15
- # Install globally from local source (for testing)
16
- npm install -g /Users/zliu/IdeaProjects/my-workflow
17
-
18
- # Install from npm
19
- npm install -g @aion0/forge
20
-
21
- # Run via npm global install
22
- forge-server # foreground (default port 3000)
23
- forge-server --dev # dev mode
24
- forge-server --background # background, logs to ~/.forge/forge.log
25
- forge-server --stop # stop background server
26
- forge-server --restart # stop + start (safe for remote)
27
- forge-server --rebuild # force rebuild
28
- forge-server --port 4000 # custom web port
29
- forge-server --terminal-port 4001 # custom terminal port
30
- forge-server --dir ~/.forge-staging # custom data directory
31
-
32
- # CLI
33
- forge # help
34
- forge password # show today's login password
35
- forge tasks # list tasks
5
+ # ── Start ──
6
+ ./start.sh # production (kill old processes → build → start)
7
+ ./start.sh dev # development (hot-reload)
8
+ forge-server # production via npm link/install
9
+ forge-server --dev # dev mode
10
+ forge-server --background # background, logs to ~/.forge/forge.log
11
+ forge-server --stop # stop background server
12
+ forge-server --restart # stop + start (safe for remote)
13
+ forge-server --rebuild # force rebuild
14
+ forge-server --port 4000 --terminal-port 4001 --dir ~/.forge-staging
15
+ forge-server --reset-terminal # kill terminal server (loses tmux attach)
16
+ forge-server --version # show version
17
+
18
+ # ── Test ──
19
+ ./dev-test.sh # test instance (port 4000, data ~/.forge-test)
20
+
21
+ # ── Install ──
22
+ ./install.sh # install from npm
23
+ ./install.sh --local # install from local source (npm link + build)
24
+
25
+ # ── Publish ──
26
+ ./publish.sh # bump patch version, commit, tag
27
+ ./publish.sh minor # bump minor
28
+ ./publish.sh 1.0.0 # explicit version
29
+ npm login && npm publish --access public --otp=<code>
30
+
31
+ # ── Monitor ──
32
+ ./check-forge-status.sh # show process status + tmux sessions
33
+
34
+ # ── CLI ──
35
+ forge # help
36
+ forge --version # show version
37
+ forge password # show today's login password
38
+ forge tasks # list tasks
36
39
  forge task <project> "prompt" # submit task
37
-
38
- # Terminal server runs on port 3001 (auto-started by Next.js)
39
- # Data directory: ~/.forge/
40
- # Config: ~/.forge/settings.yaml
41
- # Env: ~/.forge/.env.local
40
+ forge watch <id> # live stream task output
42
41
  ```
43
42
 
44
43
  ### Key Paths
@@ -46,7 +45,12 @@ forge task <project> "prompt" # submit task
46
45
  - npm package: `@aion0/forge`
47
46
  - GitHub: `github.com/aiwatching/forge`
48
47
 
48
+ ### Architecture
49
+ - `forge-server.mjs` starts: Next.js + terminal-standalone + telegram-standalone
50
+ - `pnpm dev` / `start.sh dev` starts: Next.js (init.ts spawns terminal + telegram)
51
+ - `FORGE_EXTERNAL_SERVICES=1` → init.ts skips spawning (forge-server manages them)
52
+
49
53
  ## Obsidian Vault
50
54
  Location: /Users/zliu/MyDocuments/obsidian-project/Projects/Bastion
51
55
  When I ask about my notes, use bash to search and read files from this directory.
52
- Example: find /Users/zliu/MyDocuments/obsidian-project -name "*.md" | head -20
56
+ Example: find /Users/zliu/MyDocuments/obsidian-project -name "*.md" | head -20
package/README.md CHANGED
@@ -1,73 +1,72 @@
1
1
  <p align="center">
2
- <img src="app/icon.svg" width="80" height="80" alt="Forge">
2
+ <img src="app/icon.svg" width="80" height="80" alt="Forge - Self-hosted Vibe Coding Platform">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Forge</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Self-hosted Vibe Coding platform — browser terminal, task orchestration, remote access</strong>
8
+ <strong>Self-hosted Vibe Coding platform for Claude Code — browser terminal, AI task orchestration, remote access from any device</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://www.npmjs.com/package/@aion0/forge"><img src="https://img.shields.io/npm/v/@aion0/forge" alt="npm"></a>
13
- <a href="https://github.com/aiwatching/forge/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@aion0/forge" alt="license"></a>
14
- <a href="https://github.com/aiwatching/forge"><img src="https://img.shields.io/github/stars/aiwatching/forge?style=social" alt="stars"></a>
12
+ <a href="https://www.npmjs.com/package/@aion0/forge"><img src="https://img.shields.io/npm/v/@aion0/forge" alt="npm version"></a>
13
+ <a href="https://github.com/aiwatching/forge/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@aion0/forge" alt="MIT license"></a>
14
+ <a href="https://github.com/aiwatching/forge"><img src="https://img.shields.io/github/stars/aiwatching/forge?style=social" alt="GitHub stars"></a>
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
- <a href="#installation">Install</a> · <a href="#features">Features</a> · <a href="#quick-start">Quick Start</a> · <a href="#telegram-bot">Telegram</a> · <a href="#configuration">Config</a> · <a href="#roadmap">Roadmap</a>
18
+ <a href="#installation">Install</a> · <a href="#features">Features</a> · <a href="#quick-start">Quick Start</a> · <a href="#telegram-bot">Telegram</a> · <a href="#scripts">Scripts</a> · <a href="#configuration">Config</a> · <a href="#roadmap">Roadmap</a>
19
19
  </p>
20
20
 
21
21
  ---
22
22
 
23
- Forge turns [Claude Code](https://docs.anthropic.com/en/docs/claude-code) into a full web-based vibe coding platform. Open your browser, start coding with AI from anywhere — your iPad, phone, or any device with a browser.
23
+ ## What is Forge?
24
24
 
25
- **No API keys required.** Runs on your existing Claude Code CLI subscription. Your code stays on your machine.
25
+ Forge is a self-hosted web platform that turns [Claude Code](https://docs.anthropic.com/en/docs/claude-code) into a remote-accessible vibe coding environment. It provides a persistent browser-based terminal (powered by tmux), background AI task orchestration, one-click Cloudflare Tunnel for remote access, and a Telegram bot for mobile control.
26
+
27
+ **Use cases:**
28
+ - Vibe code from your iPad, phone, or any browser — Claude Code runs on your machine, you access it from anywhere
29
+ - Submit AI coding tasks that run in the background while you sleep
30
+ - Chain multiple Claude Code instances into automated pipelines (design → implement → review → deploy)
31
+ - Browse and search your Obsidian notes with an AI assistant
32
+
33
+ **No API keys required.** Forge uses your existing Claude Code CLI subscription. Your code never leaves your machine.
26
34
 
27
35
  ## Features
28
36
 
29
- | Feature | Description |
37
+ | Feature | What it does |
30
38
  |---------|-------------|
31
- | **Vibe Coding** | Browser-based tmux terminal. Multiple tabs, persistent sessions that survive refresh, browser close, and server restart |
32
- | **Remote Access** | One-click Cloudflare Tunnel secure public URL, zero config, no account needed |
33
- | **Task Queue** | Submit tasks to Claude Code in the background. Live streaming output, cost tracking, session continuity |
34
- | **Docs Viewer** | Render Obsidian vaults / markdown directories with a dedicated Claude Console |
35
- | **Project Manager** | Browse projects, view files, git status, commit, push, pull all from the browser |
36
- | **Demo Preview** | Preview local dev servers through the tunnel with a dedicated Cloudflare URL |
37
- | **Telegram Bot** | Submit tasks, check status, control tunnel, take notes all from your phone |
38
- | **File Browser** | Code viewer with syntax highlighting, git changes, diff view, multi-repo support |
39
- | **YAML Workflows** | Define multi-step flows that chain Claude Code tasks together |
40
- | **CLI** | Full command-line interface for task management |
39
+ | **Vibe Coding** | Browser-based tmux terminal with multiple tabs. Sessions persist across page refresh, browser close, and server restart. Access from any device via Cloudflare Tunnel. |
40
+ | **AI Task Queue** | Submit prompts to Claude Code that run in the background. Live streaming output, cost tracking, session continuity across tasks. Supports `--dangerously-skip-permissions` for fully autonomous operation. |
41
+ | **Pipeline Engine** | Define multi-step DAG workflows in YAML. Chain Claude Code tasks with dependencies, output passing between steps, conditional routing, and parallel execution. Visual drag-and-drop editor included. |
42
+ | **Remote Access** | One-click Cloudflare Tunnel generates a secure public URL. Zero config, no Cloudflare account needed. Auto-health-check with reconnection. |
43
+ | **Docs Viewer** | Render Obsidian vaults and markdown directories in the browser. Built-in Claude Console for AI-assisted note-taking and research. Image support. |
44
+ | **Project Manager** | Browse project files, view code with syntax highlighting, git status/commit/push/pull, commit history — all from the browser. Multi-repo support. |
45
+ | **Demo Preview** | Preview local dev servers (Vite, Next.js, etc.) through dedicated Cloudflare Tunnel URLs. Multiple simultaneous previews supported. |
46
+ | **Telegram Bot** | Create tasks, check status, control tunnel, take notes, get AI session summaries — all from your phone. Whitelist-protected. |
47
+ | **CLI** | Full command-line interface: `forge task`, `forge watch`, `forge status`, `forge password`, and more. |
48
+ | **Monitor** | Real-time dashboard showing process status (Next.js, Terminal, Telegram, Tunnel), tmux sessions, and system uptime. |
41
49
 
42
50
  ## Installation
43
51
 
44
- ### npm (recommended)
45
-
46
52
  ```bash
47
53
  npm install -g @aion0/forge
48
54
  forge-server
49
55
  ```
50
56
 
57
+ Open `http://localhost:3000` — a login password is printed in the console.
58
+
51
59
  ### From source
52
60
 
53
61
  ```bash
54
62
  git clone https://github.com/aiwatching/forge.git
55
63
  cd forge
56
64
  pnpm install
57
- pnpm dev
65
+ ./start.sh # production
66
+ ./start.sh dev # development with hot-reload
58
67
  ```
59
68
 
60
- ### Options
61
-
62
- ```bash
63
- forge-server # Production (auto-builds if needed)
64
- forge-server --dev # Development with hot-reload
65
- forge-server --background # Run in background, logs to ~/.forge/forge.log
66
- forge-server --stop # Stop background server
67
- forge-server --rebuild # Force rebuild
68
- ```
69
-
70
- ## Prerequisites
69
+ ### Prerequisites
71
70
 
72
71
  - **Node.js** >= 20
73
72
  - **tmux** — `brew install tmux` (macOS) / `apt install tmux` (Linux)
@@ -75,68 +74,133 @@ forge-server --rebuild # Force rebuild
75
74
 
76
75
  ## Quick Start
77
76
 
78
- 1. **Start Forge**
79
-
80
- ```bash
81
- forge-server
82
- ```
83
-
84
- 2. **Open browser** → `http://localhost:3000`
85
-
86
- 3. **Log in** — password is auto-generated and printed in the console:
87
-
88
- ```
89
- [init] Login password: a7x9k2 (valid today)
90
- ```
91
-
92
- Forgot it? Run `forge password`
93
-
94
- 4. **Configure projects** — Settings → add your project directories
95
-
96
- 5. **Start vibe coding** — open a terminal tab, run `claude`, and go
77
+ 1. **Start Forge** — `forge-server` or `./start.sh`
78
+ 2. **Open browser** — `http://localhost:3000`
79
+ 3. **Log in** — password is in the console output, rotates daily. Run `forge password` if you forget.
80
+ 4. **Configure** — Settings → add project directories and (optionally) Telegram bot token
81
+ 5. **Start coding** — Open a terminal tab, run `claude`, and vibe
97
82
 
98
83
  ## Remote Access
99
84
 
100
- Access Forge from anywhere — your phone, iPad, or another computer:
85
+ Access Forge from anywhere — iPad, phone, coffee shop:
101
86
 
102
87
  1. Click the **tunnel button** in the header
103
- 2. Forge auto-downloads `cloudflared` and creates a temporary public URL
104
- 3. Open the URL on any device — protected by the daily login password
88
+ 2. A temporary Cloudflare URL is generated (no account needed)
89
+ 3. Open it on any device — protected by your daily rotating password
105
90
 
106
- > The tunnel URL changes each time. Use the Telegram `/tunnel_password` command to get it on your phone.
91
+ Health checks run every 60 seconds. If the tunnel drops, it auto-restarts and notifies you via Telegram.
107
92
 
108
93
  ## Telegram Bot
109
94
 
110
- Control Forge from your phone. Create a bot via [@BotFather](https://t.me/botfather), add the token in Settings.
95
+ Mobile-first control for Forge. Create a bot via [@BotFather](https://t.me/botfather), add the token in Settings.
111
96
 
112
97
  | Command | Description |
113
98
  |---------|-------------|
114
99
  | `/task` | Create a task (interactive project picker) |
115
100
  | `/tasks` | List tasks with quick-action numbers |
116
- | `/peek` | AI summary of a Claude session |
101
+ | `/sessions` | AI summary of Claude Code sessions |
117
102
  | `/docs` | Docs session summary or file search |
118
- | `/note` | Quick note — sent to Docs Claude |
103
+ | `/note` | Quick note — sent to Docs Claude for filing |
119
104
  | `/tunnel_start` | Start Cloudflare Tunnel |
120
105
  | `/tunnel_stop` | Stop tunnel |
121
106
  | `/tunnel_password <pw>` | Get login password + tunnel URL |
107
+ | `/watch` | Monitor session / list watchers |
108
+
109
+ Whitelist-protected — only configured Chat IDs can interact. Supports multiple users (comma-separated IDs).
110
+
111
+ ## Pipeline Engine
112
+
113
+ Define multi-step AI workflows in YAML. Each step runs Claude Code autonomously, with outputs passed to downstream steps.
114
+
115
+ ```yaml
116
+ name: feature-build
117
+ description: "Design → Implement → Review"
118
+ input:
119
+ requirement: "Feature description"
120
+ vars:
121
+ project: my-app
122
+ nodes:
123
+ architect:
124
+ project: "{{vars.project}}"
125
+ prompt: "Analyze: {{input.requirement}}. Output a design doc."
126
+ outputs:
127
+ - name: design
128
+ extract: result
129
+ implement:
130
+ project: "{{vars.project}}"
131
+ depends_on: [architect]
132
+ prompt: "Implement: {{nodes.architect.outputs.design}}"
133
+ outputs:
134
+ - name: diff
135
+ extract: git_diff
136
+ review:
137
+ depends_on: [implement]
138
+ project: "{{vars.project}}"
139
+ prompt: "Review: {{nodes.implement.outputs.diff}}"
140
+ ```
122
141
 
123
- Whitelist-protected only configured Chat IDs can interact with the bot.
142
+ Features: DAG execution, parallel nodes, conditional routing, loop protection, Telegram notifications per step, visual editor.
124
143
 
125
144
  ## CLI
126
145
 
146
+ All commands are unified under `forge`:
147
+
127
148
  ```bash
149
+ # Server management
150
+ forge server start # Start server (foreground)
151
+ forge server start --background # Start in background
152
+ forge server start --dev # Development mode with hot-reload
153
+ forge server start --port 4000 # Custom port
154
+ forge server stop # Stop server
155
+ forge server restart # Restart (safe for remote use)
156
+ forge server rebuild # Force rebuild
157
+
158
+ # Tasks
128
159
  forge task <project> <prompt> # Submit a task
129
- forge tasks [status] # List tasks
130
- forge watch <id> # Live stream output
131
- forge status <id> # Task details + result
160
+ forge tasks [status] # List tasks (running|queued|done|failed)
161
+ forge watch <id> # Live stream task output
132
162
  forge cancel <id> # Cancel a task
133
163
  forge retry <id> # Retry a failed task
164
+
165
+ # Workflows
134
166
  forge run <flow-name> # Run a YAML workflow
135
- forge projects # List projects
167
+ forge flows # List available workflows
168
+
169
+ # Status & Info
170
+ forge status # Show process status + tmux sessions
171
+ forge status <id> # Show task details
136
172
  forge password # Show login password
173
+ forge projects # List configured projects
174
+ forge -v # Show version
175
+
176
+ # Package management
177
+ forge upgrade # Update to latest npm version
178
+ forge uninstall # Stop server + uninstall (data preserved in ~/.forge)
137
179
  ```
138
180
 
139
- Shortcuts: `t`=task, `ls`=tasks, `w`=watch, `s`=status, `f`=flows, `p`=projects, `pw`=password
181
+ Shortcuts: `t`=task, `ls`=tasks, `w`=watch, `s`=status, `l`=log, `f`=flows, `p`=projects, `pw`=password
182
+
183
+ ### Server start options
184
+
185
+ ```bash
186
+ forge server start --port 4000 # Custom web port (default: 3000)
187
+ forge server start --terminal-port 4001 # Custom terminal port (default: 3001)
188
+ forge server start --dir ~/.forge-test # Custom data directory
189
+ forge server start --background # Run in background
190
+ forge server start --reset-terminal # Kill terminal server on start
191
+ ```
192
+
193
+ ### Development scripts
194
+
195
+ ```bash
196
+ ./start.sh # kill old processes → build → start (production)
197
+ ./start.sh dev # development with hot-reload
198
+ ./dev-test.sh # test instance on port 4000 (separate data dir)
199
+ ./install.sh # install from npm
200
+ ./install.sh --local # install from local source
201
+ ./publish.sh # bump version → commit → tag → ready to publish
202
+ ./check-forge-status.sh # show all forge processes + tmux sessions
203
+ ```
140
204
 
141
205
  ## Configuration
142
206
 
@@ -144,14 +208,16 @@ All data lives in `~/.forge/`:
144
208
 
145
209
  ```
146
210
  ~/.forge/
147
- ├── .env.local # Environment variables (optional)
211
+ ├── .env.local # Environment variables (AUTH_SECRET, API keys)
148
212
  ├── settings.yaml # Main configuration
149
- ├── password.json # Daily auto-generated password
150
- ├── data.db # SQLite database
213
+ ├── password.json # Daily auto-generated login password
214
+ ├── data.db # SQLite database (tasks, sessions)
151
215
  ├── terminal-state.json # Terminal tab layout
216
+ ├── tunnel-state.json # Tunnel process state
152
217
  ├── preview.json # Demo preview config
218
+ ├── pipelines/ # Pipeline execution state
153
219
  ├── flows/ # YAML workflow definitions
154
- └── bin/ # Auto-downloaded binaries
220
+ └── bin/ # Auto-downloaded binaries (cloudflared)
155
221
  ```
156
222
 
157
223
  <details>
@@ -169,6 +235,10 @@ telegramChatId: "" # Comma-separated for multiple users
169
235
  telegramTunnelPassword: ""
170
236
  notifyOnComplete: true
171
237
  notifyOnFailure: true
238
+ taskModel: default # default / sonnet / opus / haiku
239
+ pipelineModel: default
240
+ telegramModel: sonnet
241
+ skipPermissions: false # Add --dangerously-skip-permissions to terminal claude invocations
172
242
  ```
173
243
 
174
244
  </details>
@@ -190,45 +260,41 @@ AUTH_SECRET=<random-string>
190
260
  ## Architecture
191
261
 
192
262
  ```
193
- ┌──────────────────────────────────────────────────┐
194
- │ Web Dashboard (Next.js 16 + React 19)
195
- ┌─────────┐ ┌──────┐ ┌────────┐ ┌───────────┐ │
196
- │ Vibe Docs │Projects│ │Demo │ │
197
- Coding │ │ │ │ │ │Preview │ │
198
- └─────────┘ └──────┘ └────────┘ └───────────┘ │
199
- ├──────────────────────────────────────────────────┤
200
- API Layer (Next.js Route Handlers)
201
- ├───────────┬───────────┬──────────────────────────┤
202
- │ Claude │ Task │ Telegram Bot │
203
- │ Code │ Runner │ + Notifications │
204
- │ Process │ (Queue) │ │
205
- ├───────────┴───────────┴──────────────────────────┤
206
- │ SQLite · Terminal Server · Cloudflare Tunnel │
207
- └──────────────────────────────────────────────────┘
263
+ forge-server.mjs (single process)
264
+ ├── Next.js (web dashboard + API)
265
+ ├── Vibe Coding (xterm.js + tmux)
266
+ ├── Docs Viewer (markdown + Claude Console)
267
+ ├── Project Manager (files + git)
268
+ ├── Task Queue (background Claude Code)
269
+ │ ├── Pipeline Engine (DAG workflows)
270
+ ├── Demo Preview (tunnel proxy)
271
+ │ └── Monitor (process status)
272
+ ├── terminal-standalone.ts (WebSocket → tmux)
273
+ ├── telegram-standalone.ts (Telegram Bot API polling)
274
+ └── cloudflared (Cloudflare Tunnel, on demand)
208
275
  ```
209
276
 
210
277
  ## Tech Stack
211
278
 
212
279
  | Layer | Technology |
213
280
  |-------|-----------|
214
- | Frontend | Next.js 16, React 19, Tailwind CSS 4, xterm.js |
281
+ | Frontend | Next.js 16, React 19, Tailwind CSS 4, xterm.js, ReactFlow |
215
282
  | Backend | Next.js Route Handlers, SQLite (better-sqlite3) |
216
283
  | Terminal | node-pty, tmux, WebSocket |
217
284
  | Auth | NextAuth v5 (daily rotating password + OAuth) |
218
285
  | Tunnel | Cloudflare cloudflared (zero-config) |
219
286
  | Bot | Telegram Bot API |
287
+ | Pipeline | YAML-based DAG engine with visual editor |
220
288
 
221
289
  ## Troubleshooting
222
290
 
223
291
  <details>
224
292
  <summary><strong>macOS: "fork failed: Device not configured"</strong></summary>
225
293
 
226
- PTY device limit exhausted. Increase it:
294
+ PTY device limit exhausted:
227
295
 
228
296
  ```bash
229
297
  sudo sysctl kern.tty.ptmx_max=2048
230
-
231
- # Permanent
232
298
  echo 'kern.tty.ptmx_max=2048' | sudo tee -a /etc/sysctl.conf
233
299
  ```
234
300
 
@@ -237,7 +303,7 @@ echo 'kern.tty.ptmx_max=2048' | sudo tee -a /etc/sysctl.conf
237
303
  <details>
238
304
  <summary><strong>Session cookie invalid after restart</strong></summary>
239
305
 
240
- Fix the AUTH_SECRET so it persists:
306
+ Fix AUTH_SECRET so it persists across restarts:
241
307
 
242
308
  ```bash
243
309
  echo "AUTH_SECRET=$(openssl rand -hex 32)" >> ~/.forge/.env.local
@@ -245,13 +311,25 @@ echo "AUTH_SECRET=$(openssl rand -hex 32)" >> ~/.forge/.env.local
245
311
 
246
312
  </details>
247
313
 
314
+ <details>
315
+ <summary><strong>Orphan processes after Ctrl+C</strong></summary>
316
+
317
+ Use `./start.sh` or `forge-server` which clean up old processes on start. Or manually:
318
+
319
+ ```bash
320
+ ./check-forge-status.sh # see what's running
321
+ pkill -f 'telegram-standalone|terminal-standalone|next-server|cloudflared'
322
+ ```
323
+
324
+ </details>
325
+
248
326
  ## Roadmap
249
327
 
250
- - [ ] **Multi-Agent Workflow** — DAG-based pipelines where multiple Claude Code instances collaborate ([design doc](docs/roadmap-multi-agent-workflow.md))
251
- - [ ] Pipeline UI — DAG visualization with real-time node status
328
+ - [ ] **Multi-Agent Collaboration** — Real-time message channels between concurrent Claude Code instances ([design doc](docs/roadmap-multi-agent-workflow.md))
252
329
  - [ ] Additional bot platforms — Discord, Slack
253
330
  - [ ] Excalidraw rendering in Docs viewer
254
- - [ ] Multi-model chat (Anthropic, OpenAI, Google, xAI)
331
+ - [ ] Multi-model chat with API keys (Anthropic, OpenAI, Google, xAI)
332
+ - [ ] Plugin system for custom integrations
255
333
 
256
334
  ## Contributing
257
335
 
@@ -0,0 +1,58 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { execSync } from 'node:child_process';
3
+
4
+ function run(cmd: string): string {
5
+ try {
6
+ return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
7
+ } catch { return ''; }
8
+ }
9
+
10
+ function countProcess(pattern: string): { count: number; pid: string } {
11
+ const out = run(`ps aux | grep '${pattern}' | grep -v grep | head -1`);
12
+ const pid = out ? out.split(/\s+/)[1] || '' : '';
13
+ const count = out ? run(`ps aux | grep '${pattern}' | grep -v grep | wc -l`).trim() : '0';
14
+ return { count: parseInt(count), pid };
15
+ }
16
+
17
+ export async function GET() {
18
+ // Processes
19
+ const nextjs = countProcess('next-server');
20
+ const terminal = countProcess('terminal-standalone');
21
+ const telegram = countProcess('telegram-standalone');
22
+ const tunnel = countProcess('cloudflared tunnel');
23
+
24
+ // Tunnel URL
25
+ let tunnelUrl = '';
26
+ try {
27
+ const { readFileSync } = require('fs');
28
+ const { join } = require('path');
29
+ const { homedir } = require('os');
30
+ const state = JSON.parse(readFileSync(join(homedir(), '.forge', 'tunnel-state.json'), 'utf-8'));
31
+ tunnelUrl = state.url || '';
32
+ } catch {}
33
+
34
+ // tmux sessions
35
+ let sessions: { name: string; created: string; attached: boolean; windows: number }[] = [];
36
+ try {
37
+ const out = run("tmux list-sessions -F '#{session_name}||#{session_created}||#{session_attached}||#{session_windows}' 2>/dev/null");
38
+ sessions = out.split('\n').filter(l => l.startsWith('mw-')).map(line => {
39
+ const [name, created, attached, windows] = line.split('||');
40
+ return { name, created: new Date(Number(created) * 1000).toISOString(), attached: attached !== '0', windows: Number(windows) || 1 };
41
+ });
42
+ } catch {}
43
+
44
+ // System info
45
+ const uptime = run('uptime');
46
+ const memory = run("ps -o rss= -p $$ 2>/dev/null || echo 0");
47
+
48
+ return NextResponse.json({
49
+ processes: {
50
+ nextjs: { running: nextjs.count > 0, pid: nextjs.pid },
51
+ terminal: { running: terminal.count > 0, pid: terminal.pid },
52
+ telegram: { running: telegram.count > 0, pid: telegram.pid },
53
+ tunnel: { running: tunnel.count > 0, pid: tunnel.pid, url: tunnelUrl },
54
+ },
55
+ sessions,
56
+ uptime: uptime.replace(/.*up\s+/, '').replace(/,\s+\d+ user.*/, '').trim(),
57
+ });
58
+ }
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # check-forge-status.sh — Show Forge process status
3
+
4
+ echo "══════════════════════════════════"
5
+ echo " Forge Process Status"
6
+ echo "══════════════════════════════════"
7
+
8
+ # Next.js
9
+ count=$(ps aux | grep 'next-server' | grep -v grep | wc -l | tr -d ' ')
10
+ pid=$(ps aux | grep 'next-server' | grep -v grep | awk '{print $2}' | head -1)
11
+ if [ "$count" -gt 0 ]; then
12
+ echo " ● Next.js running (pid: $pid)"
13
+ else
14
+ echo " ○ Next.js stopped"
15
+ fi
16
+
17
+ # Terminal
18
+ count=$(ps aux | grep 'terminal-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
19
+ pid=$(ps aux | grep 'terminal-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
20
+ if [ "$count" -gt 0 ]; then
21
+ echo " ● Terminal running (pid: $pid)"
22
+ else
23
+ echo " ○ Terminal stopped"
24
+ fi
25
+
26
+ # Telegram
27
+ count=$(ps aux | grep 'telegram-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
28
+ pid=$(ps aux | grep 'telegram-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
29
+ if [ "$count" -gt 0 ]; then
30
+ echo " ● Telegram running (pid: $pid)"
31
+ else
32
+ echo " ○ Telegram stopped"
33
+ fi
34
+
35
+ # Cloudflare Tunnel
36
+ count=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | wc -l | tr -d ' ')
37
+ pid=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | awk '{print $2}' | head -1)
38
+ url=$(cat ~/.forge/tunnel-state.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('url',''))" 2>/dev/null)
39
+ if [ "$count" -gt 0 ]; then
40
+ echo " ● Tunnel running (pid: $pid) ${url}"
41
+ else
42
+ echo " ○ Tunnel stopped"
43
+ fi
44
+
45
+ # tmux sessions
46
+ tmux_count=$(tmux list-sessions 2>/dev/null | grep '^mw-' | wc -l | tr -d ' ')
47
+ echo ""
48
+ echo " Terminal sessions: $tmux_count"
49
+ tmux list-sessions 2>/dev/null | grep '^mw-' | while read line; do
50
+ echo " $line"
51
+ done
52
+
53
+ echo "══════════════════════════════════"
package/cli/mw.ts CHANGED
@@ -112,10 +112,9 @@ async function main() {
112
112
  break;
113
113
  }
114
114
 
115
- case 'status':
116
115
  case 's': {
117
116
  const id = args[0];
118
- if (!id) { console.log('Usage: mw status <id>'); process.exit(1); }
117
+ if (!id) { console.log('Usage: forge status <id>'); process.exit(1); }
119
118
  const task = await api(`/api/tasks/${id}`);
120
119
  console.log(`Task: ${task.id}`);
121
120
  console.log(`Project: ${task.projectName} (${task.projectPath})`);
@@ -353,34 +352,163 @@ async function main() {
353
352
  break;
354
353
  }
355
354
 
355
+ case 'server': {
356
+ // Delegate to forge-server.mjs
357
+ const { execSync } = await import('node:child_process');
358
+ const { join, dirname } = await import('node:path');
359
+ const { fileURLToPath } = await import('node:url');
360
+ const serverScript = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'forge-server.mjs');
361
+ const sub = args[0] || 'start';
362
+ const serverArgs = args.slice(1);
363
+
364
+ const flagMap: Record<string, string[]> = {
365
+ 'start': [],
366
+ 'stop': ['--stop'],
367
+ 'restart': ['--restart'],
368
+ 'rebuild': ['--rebuild'],
369
+ 'dev': ['--dev'],
370
+ };
371
+
372
+ const flags = flagMap[sub] || [];
373
+ const allArgs = [...flags, ...serverArgs];
374
+
375
+ try {
376
+ execSync(`node ${serverScript} ${allArgs.join(' ')}`, { stdio: 'inherit' });
377
+ } catch {}
378
+ break;
379
+ }
380
+
381
+ case 'status': {
382
+ // If arg provided, show task details
383
+ if (args[0]) {
384
+ const task = await api(`/api/tasks/${args[0]}`);
385
+ console.log(`Task: ${task.id}`);
386
+ console.log(`Project: ${task.projectName} (${task.projectPath})`);
387
+ console.log(`Status: ${task.status}`);
388
+ console.log(`Prompt: ${task.prompt}`);
389
+ if (task.startedAt) console.log(`Started: ${task.startedAt}`);
390
+ if (task.completedAt) console.log(`Completed: ${task.completedAt}`);
391
+ if (task.costUSD != null) console.log(`Cost: $${task.costUSD.toFixed(4)}`);
392
+ if (task.error) console.log(`Error: ${task.error}`);
393
+ if (task.resultSummary) console.log(`\nResult:\n${task.resultSummary}`);
394
+ if (task.gitDiff) console.log(`\nGit Diff:\n${task.gitDiff.slice(0, 2000)}`);
395
+ break;
396
+ }
397
+
398
+ // No arg — show process status
399
+ const { execSync } = await import('node:child_process');
400
+
401
+ const check = (pattern: string) => {
402
+ try {
403
+ const out = execSync(`ps aux | grep '${pattern}' | grep -v grep | head -1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
404
+ return out ? out.split(/\s+/)[1] : null;
405
+ } catch { return null; }
406
+ };
407
+
408
+ const nextPid = check('next-server');
409
+ const termPid = check('terminal-standalone');
410
+ const telePid = check('telegram-standalone');
411
+ const tunnPid = check('cloudflared tunnel');
412
+
413
+ console.log('');
414
+ console.log(` ${nextPid ? '●' : '○'} Next.js ${nextPid ? `running (pid: ${nextPid})` : 'stopped'}`);
415
+ console.log(` ${termPid ? '●' : '○'} Terminal ${termPid ? `running (pid: ${termPid})` : 'stopped'}`);
416
+ console.log(` ${telePid ? '●' : '○'} Telegram ${telePid ? `running (pid: ${telePid})` : 'stopped'}`);
417
+ console.log(` ${tunnPid ? '●' : '○'} Tunnel ${tunnPid ? `running (pid: ${tunnPid})` : 'stopped'}`);
418
+
419
+ try {
420
+ const { execSync: ex } = await import('node:child_process');
421
+ const sessions = ex("tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null", { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
422
+ .trim().split('\n').filter(l => l.startsWith('mw-'));
423
+ console.log(`\n Sessions: ${sessions.length}`);
424
+ for (const s of sessions) {
425
+ const [name, att] = s.split(' ');
426
+ console.log(` ${att !== '0' ? '●' : '○'} ${name}`);
427
+ }
428
+ } catch {
429
+ console.log('\n Sessions: 0');
430
+ }
431
+ console.log('');
432
+ break;
433
+ }
434
+
435
+ case 'upgrade': {
436
+ const { execSync } = await import('node:child_process');
437
+ const { lstatSync } = await import('node:fs');
438
+ const { join, dirname } = await import('node:path');
439
+ const { fileURLToPath } = await import('node:url');
440
+
441
+ // Check if installed via npm link (symlink)
442
+ const cliDir = dirname(fileURLToPath(import.meta.url));
443
+ let isLinked = false;
444
+ try { isLinked = lstatSync(join(cliDir, '..')).isSymbolicLink(); } catch {}
445
+
446
+ if (isLinked) {
447
+ console.log('[forge] Installed via npm link (local source)');
448
+ console.log('[forge] Pull latest and rebuild:');
449
+ console.log(' cd ' + join(cliDir, '..'));
450
+ console.log(' git pull && pnpm install && pnpm build');
451
+ } else {
452
+ console.log('[forge] Upgrading from npm...');
453
+ try {
454
+ execSync('cd /tmp && npm install -g @aion0/forge', { stdio: 'inherit' });
455
+ console.log('[forge] Upgraded. Run: forge server restart');
456
+ } catch {
457
+ console.log('[forge] Upgrade failed');
458
+ }
459
+ }
460
+ break;
461
+ }
462
+
463
+ case 'uninstall': {
464
+ const { execSync } = await import('node:child_process');
465
+ console.log('[forge] Stopping server...');
466
+ try { execSync('forge server stop', { stdio: 'inherit' }); } catch {}
467
+ console.log('[forge] Uninstalling...');
468
+ try {
469
+ execSync('npm uninstall -g @aion0/forge', { stdio: 'inherit' });
470
+ console.log('[forge] Uninstalled. Data remains in ~/.forge/');
471
+ } catch {
472
+ console.log('[forge] Uninstall failed');
473
+ }
474
+ break;
475
+ }
476
+
356
477
  default:
357
478
  console.log(`forge — Forge CLI (@aion0/forge)
358
479
 
359
480
  Usage:
360
- forge task <project> <prompt> Submit a task (auto-continues project session)
361
- forge task <project> <prompt> --new Force a fresh session
362
- forge run <flow-name> Run a YAML workflow
363
- forge tasks [status] List tasks (running|queued|done|failed)
364
- forge watch <id> Live stream task output
365
- forge log <id> Show execution log
366
- forge status <id> Task details + result
367
- forge session [project] Show session IDs → local claude --resume
368
- forge session link <project> <id> Link a local CLI session to the web system
369
- forge cancel <id> Cancel a task
370
- forge retry <id> Retry a failed task
371
- forge flows List workflows
372
- forge projects List projects
373
- forge password Show login password
374
-
375
- Shortcuts: t=task, r=run, ls=tasks, w=watch, l=log, s=status, f=flows, p=projects, pw=password
376
-
377
- Examples:
378
- forge task accord "Fix the authentication bug in login.ts"
379
- forge watch abc123
380
- forge run daily-review
381
- forge tasks running
382
- forge session accord Show session ID, then:
383
- cd ~/IdeaProjects/accord && claude --resume <session-id>`);
481
+ forge server start [options] Start server (default: foreground)
482
+ forge server stop Stop server
483
+ forge server restart Restart server (safe for remote)
484
+ forge server dev Start in dev mode
485
+ forge server rebuild Force rebuild
486
+
487
+ forge task <project> <prompt> Submit a task
488
+ forge tasks [status] List tasks
489
+ forge watch <id> Live stream output
490
+ forge status [<id>] Process status / task details
491
+ forge log <id> Show execution log
492
+ forge cancel <id> Cancel a task
493
+ forge retry <id> Retry a failed task
494
+
495
+ forge run <flow> Run a workflow
496
+ forge flows List workflows
497
+ forge projects List projects
498
+ forge session [project] Show session info
499
+ forge password Show login password
500
+
501
+ forge upgrade Update to latest version
502
+ forge uninstall Remove forge
503
+
504
+ Options for 'forge server start':
505
+ --port 4000 Custom web port (default: 3000)
506
+ --terminal-port 4001 Custom terminal port (default: 3001)
507
+ --dir ~/.forge-staging Custom data directory
508
+ --background Run in background
509
+ --reset-terminal Kill terminal server on start
510
+
511
+ Shortcuts: t=task, ls=tasks, w=watch, s=status, l=log, f=flows, p=projects, pw=password`);
384
512
  }
385
513
  }
386
514
 
@@ -8,6 +8,7 @@ import SessionView from './SessionView';
8
8
  import NewTaskModal from './NewTaskModal';
9
9
  import SettingsModal from './SettingsModal';
10
10
  import TunnelToggle from './TunnelToggle';
11
+ import MonitorPanel from './MonitorPanel';
11
12
  import type { Task } from '@/src/types';
12
13
  import type { WebTerminalHandle } from './WebTerminal';
13
14
 
@@ -44,6 +45,7 @@ export default function Dashboard({ user }: { user: any }) {
44
45
  const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
45
46
  const [showNewTask, setShowNewTask] = useState(false);
46
47
  const [showSettings, setShowSettings] = useState(false);
48
+ const [showMonitor, setShowMonitor] = useState(false);
47
49
  const [usage, setUsage] = useState<UsageSummary[]>([]);
48
50
  const [providers, setProviders] = useState<ProviderInfo[]>([]);
49
51
  const [projects, setProjects] = useState<ProjectInfo[]>([]);
@@ -194,6 +196,12 @@ export default function Dashboard({ user }: { user: any }) {
194
196
  )}
195
197
  </span>
196
198
  )}
199
+ <button
200
+ onClick={() => setShowMonitor(true)}
201
+ className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
202
+ >
203
+ Monitor
204
+ </button>
197
205
  <button
198
206
  onClick={() => setShowSettings(true)}
199
207
  className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
@@ -365,6 +373,8 @@ export default function Dashboard({ user }: { user: any }) {
365
373
  />
366
374
  )}
367
375
 
376
+ {showMonitor && <MonitorPanel onClose={() => setShowMonitor(false)} />}
377
+
368
378
  {showSettings && (
369
379
  <SettingsModal onClose={() => { setShowSettings(false); fetchData(); }} />
370
380
  )}
@@ -22,6 +22,13 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
22
22
  const [connected, setConnected] = useState(false);
23
23
  const wsRef = useRef<WebSocket | null>(null);
24
24
  const docRootRef = useRef(docRoot);
25
+ const skipPermRef = useRef(false);
26
+
27
+ useEffect(() => {
28
+ fetch('/api/settings').then(r => r.json())
29
+ .then((s: any) => { if (s.skipPermissions) skipPermRef.current = true; })
30
+ .catch(() => {});
31
+ }, []);
25
32
  docRootRef.current = docRoot;
26
33
 
27
34
  useEffect(() => {
@@ -78,7 +85,8 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
78
85
  isNewSession = false;
79
86
  setTimeout(() => {
80
87
  if (socket.readyState === WebSocket.OPEN) {
81
- socket.send(JSON.stringify({ type: 'input', data: `cd "${docRootRef.current}" && claude --resume\n` }));
88
+ const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
89
+ socket.send(JSON.stringify({ type: 'input', data: `cd "${docRootRef.current}" && claude --resume${sf}\n` }));
82
90
  }
83
91
  }, 300);
84
92
  }
@@ -147,13 +155,13 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
147
155
  </span>
148
156
  <div className="ml-auto flex items-center gap-1">
149
157
  <button
150
- onClick={() => runCommand(`cd "${docRoot}" && claude`)}
158
+ onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude${sf}`); }}
151
159
  className="text-[10px] px-2 py-0.5 text-[var(--accent)] hover:bg-[#2a2a4a] rounded"
152
160
  >
153
161
  New
154
162
  </button>
155
163
  <button
156
- onClick={() => runCommand(`cd "${docRoot}" && claude --resume`)}
164
+ onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude --resume${sf}`); }}
157
165
  className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[#2a2a4a] rounded"
158
166
  >
159
167
  Resume
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+
5
+ interface MonitorData {
6
+ processes: {
7
+ nextjs: { running: boolean; pid: string };
8
+ terminal: { running: boolean; pid: string };
9
+ telegram: { running: boolean; pid: string };
10
+ tunnel: { running: boolean; pid: string; url: string };
11
+ };
12
+ sessions: { name: string; created: string; attached: boolean; windows: number }[];
13
+ uptime: string;
14
+ }
15
+
16
+ export default function MonitorPanel({ onClose }: { onClose: () => void }) {
17
+ const [data, setData] = useState<MonitorData | null>(null);
18
+
19
+ const refresh = useCallback(() => {
20
+ fetch('/api/monitor').then(r => r.json()).then(setData).catch(() => {});
21
+ }, []);
22
+
23
+ useEffect(() => {
24
+ refresh();
25
+ const timer = setInterval(refresh, 5000);
26
+ return () => clearInterval(timer);
27
+ }, [refresh]);
28
+
29
+ return (
30
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
31
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[500px] max-h-[80vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
32
+ <div className="px-4 py-3 border-b border-[var(--border)] flex items-center justify-between">
33
+ <h2 className="text-sm font-bold text-[var(--text-primary)]">Monitor</h2>
34
+ <div className="flex items-center gap-2">
35
+ <button onClick={refresh} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">↻</button>
36
+ <button onClick={onClose} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">Close</button>
37
+ </div>
38
+ </div>
39
+
40
+ {data ? (
41
+ <div className="p-4 space-y-4">
42
+ {/* Processes */}
43
+ <div>
44
+ <h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-2">Processes</h3>
45
+ <div className="space-y-1.5">
46
+ {[
47
+ { label: 'Next.js', ...data.processes.nextjs },
48
+ { label: 'Terminal Server', ...data.processes.terminal },
49
+ { label: 'Telegram Bot', ...data.processes.telegram },
50
+ { label: 'Tunnel', ...data.processes.tunnel },
51
+ ].map(p => (
52
+ <div key={p.label} className="flex items-center gap-2 text-xs">
53
+ <span className={p.running ? 'text-green-400' : 'text-gray-500'}>●</span>
54
+ <span className="text-[var(--text-primary)] w-28">{p.label}</span>
55
+ {p.running ? (
56
+ <span className="text-[var(--text-secondary)] font-mono text-[10px]">pid: {p.pid}</span>
57
+ ) : (
58
+ <span className="text-gray-500 text-[10px]">stopped</span>
59
+ )}
60
+ </div>
61
+ ))}
62
+ {data.processes.tunnel.running && data.processes.tunnel.url && (
63
+ <div className="pl-6 text-[10px] text-[var(--accent)] truncate">{data.processes.tunnel.url}</div>
64
+ )}
65
+ </div>
66
+ </div>
67
+
68
+ {/* Uptime */}
69
+ {data.uptime && (
70
+ <div className="text-[10px] text-[var(--text-secondary)]">
71
+ Uptime: {data.uptime}
72
+ </div>
73
+ )}
74
+
75
+ {/* Sessions */}
76
+ <div>
77
+ <h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-2">
78
+ Terminal Sessions ({data.sessions.length})
79
+ </h3>
80
+ {data.sessions.length === 0 ? (
81
+ <div className="text-[10px] text-[var(--text-secondary)]">No sessions</div>
82
+ ) : (
83
+ <div className="space-y-1">
84
+ {data.sessions.map(s => (
85
+ <div key={s.name} className="flex items-center gap-2 text-[11px]">
86
+ <span className={s.attached ? 'text-green-400' : 'text-yellow-500'}>●</span>
87
+ <span className="font-mono text-[var(--text-primary)] truncate flex-1">{s.name}</span>
88
+ <span className="text-[9px] text-[var(--text-secondary)]">{s.attached ? 'attached' : 'detached'}</span>
89
+ <span className="text-[9px] text-[var(--text-secondary)]">{new Date(s.created).toLocaleTimeString()}</span>
90
+ </div>
91
+ ))}
92
+ </div>
93
+ )}
94
+ </div>
95
+ </div>
96
+ ) : (
97
+ <div className="p-8 text-center text-xs text-[var(--text-secondary)]">Loading...</div>
98
+ )}
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -42,6 +42,7 @@ interface Settings {
42
42
  taskModel: string;
43
43
  pipelineModel: string;
44
44
  telegramModel: string;
45
+ skipPermissions: boolean;
45
46
  }
46
47
 
47
48
  interface TunnelStatus {
@@ -66,6 +67,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
66
67
  taskModel: 'sonnet',
67
68
  pipelineModel: 'sonnet',
68
69
  telegramModel: 'sonnet',
70
+ skipPermissions: false,
69
71
  });
70
72
  const [newRoot, setNewRoot] = useState('');
71
73
  const [newDocRoot, setNewDocRoot] = useState('');
@@ -347,6 +349,22 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
347
349
  </div>
348
350
  </div>
349
351
 
352
+ {/* Permissions */}
353
+ <div className="space-y-2">
354
+ <label className="flex items-center gap-2 text-xs text-[var(--text-primary)] cursor-pointer">
355
+ <input
356
+ type="checkbox"
357
+ checked={settings.skipPermissions || false}
358
+ onChange={e => setSettings({ ...settings, skipPermissions: e.target.checked })}
359
+ className="rounded"
360
+ />
361
+ Skip permissions check (--dangerously-skip-permissions)
362
+ </label>
363
+ <p className="text-[9px] text-[var(--text-secondary)]">
364
+ When enabled, all Claude Code tasks and pipelines run without permission prompts. Useful for background automation but less safe.
365
+ </p>
366
+ </div>
367
+
350
368
  {/* Remote Access (Cloudflare Tunnel) */}
351
369
  <div className="space-y-2">
352
370
  <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
@@ -182,6 +182,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
182
182
  const [showNewTabModal, setShowNewTabModal] = useState(false);
183
183
  const [projectRoots, setProjectRoots] = useState<string[]>([]);
184
184
  const [allProjects, setAllProjects] = useState<{ name: string; path: string; root: string }[]>([]);
185
+ const [skipPermissions, setSkipPermissions] = useState(false);
185
186
  const [expandedRoot, setExpandedRoot] = useState<string | null>(null);
186
187
 
187
188
  // Restore shared state from server after mount
@@ -196,6 +197,10 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
196
197
  }
197
198
  setHydrated(true);
198
199
  });
200
+ // Fetch settings for skipPermissions
201
+ fetch('/api/settings').then(r => r.json())
202
+ .then((s: any) => { if (s.skipPermissions) setSkipPermissions(true); })
203
+ .catch(() => {});
199
204
  // Fetch projects and derive roots
200
205
  fetch('/api/projects').then(r => r.json())
201
206
  .then((p: { name: string; path: string; root: string }[]) => {
@@ -765,6 +770,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
765
770
  setRatios={tab.id === activeTabId ? setRatios : () => {}}
766
771
  onSessionConnected={onSessionConnected}
767
772
  refreshKeys={refreshKeys}
773
+ skipPermissions={skipPermissions}
768
774
  />
769
775
  </div>
770
776
  ))}
@@ -777,7 +783,7 @@ export default WebTerminal;
777
783
  // ─── Pane renderer ───────────────────────────────────────────
778
784
 
779
785
  function PaneRenderer({
780
- node, activeId, onFocus, ratios, setRatios, onSessionConnected, refreshKeys,
786
+ node, activeId, onFocus, ratios, setRatios, onSessionConnected, refreshKeys, skipPermissions,
781
787
  }: {
782
788
  node: SplitNode;
783
789
  activeId: number;
@@ -786,11 +792,12 @@ function PaneRenderer({
786
792
  setRatios: React.Dispatch<React.SetStateAction<Record<number, number>>>;
787
793
  onSessionConnected: (paneId: number, sessionName: string) => void;
788
794
  refreshKeys: Record<number, number>;
795
+ skipPermissions?: boolean;
789
796
  }) {
790
797
  if (node.type === 'terminal') {
791
798
  return (
792
799
  <div className={`h-full w-full ${activeId === node.id ? 'ring-1 ring-[#7c5bf0]/50 ring-inset' : ''}`} onMouseDown={() => onFocus(node.id)}>
793
- <MemoTerminalPane key={`${node.id}-${refreshKeys[node.id] || 0}`} id={node.id} sessionName={node.sessionName} projectPath={node.projectPath} onSessionConnected={onSessionConnected} />
800
+ <MemoTerminalPane key={`${node.id}-${refreshKeys[node.id] || 0}`} id={node.id} sessionName={node.sessionName} projectPath={node.projectPath} skipPermissions={skipPermissions} onSessionConnected={onSessionConnected} />
794
801
  </div>
795
802
  );
796
803
  }
@@ -799,8 +806,8 @@ function PaneRenderer({
799
806
 
800
807
  return (
801
808
  <DraggableSplit splitId={node.id} direction={node.direction} ratio={ratio} setRatios={setRatios}>
802
- <PaneRenderer node={node.first} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} />
803
- <PaneRenderer node={node.second} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} />
809
+ <PaneRenderer node={node.first} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} skipPermissions={skipPermissions} />
810
+ <PaneRenderer node={node.second} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} skipPermissions={skipPermissions} />
804
811
  </DraggableSplit>
805
812
  );
806
813
  }
@@ -921,17 +928,21 @@ const MemoTerminalPane = memo(function TerminalPane({
921
928
  id,
922
929
  sessionName,
923
930
  projectPath,
931
+ skipPermissions,
924
932
  onSessionConnected,
925
933
  }: {
926
934
  id: number;
927
935
  sessionName?: string;
928
936
  projectPath?: string;
937
+ skipPermissions?: boolean;
929
938
  onSessionConnected: (paneId: number, sessionName: string) => void;
930
939
  }) {
931
940
  const containerRef = useRef<HTMLDivElement>(null);
932
941
  const sessionNameRef = useRef(sessionName);
933
942
  sessionNameRef.current = sessionName;
934
943
  const projectPathRef = useRef(projectPath);
944
+ const skipPermRef = useRef(skipPermissions);
945
+ skipPermRef.current = skipPermissions;
935
946
  projectPathRef.current = projectPath;
936
947
 
937
948
  useEffect(() => {
@@ -1055,7 +1066,8 @@ const MemoTerminalPane = memo(function TerminalPane({
1055
1066
  isNewlyCreated = false;
1056
1067
  setTimeout(() => {
1057
1068
  if (!disposed && ws?.readyState === WebSocket.OPEN) {
1058
- ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude --resume\n` }));
1069
+ const skipFlag = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
1070
+ ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude --resume${skipFlag}\n` }));
1059
1071
  }
1060
1072
  }, 300);
1061
1073
  }
package/lib/settings.ts CHANGED
@@ -19,6 +19,7 @@ export interface Settings {
19
19
  taskModel: string; // Model for tasks (default: sonnet)
20
20
  pipelineModel: string; // Model for pipelines (default: sonnet)
21
21
  telegramModel: string; // Model for Telegram AI features (default: sonnet)
22
+ skipPermissions: boolean; // Add --dangerously-skip-permissions to all claude invocations
22
23
  }
23
24
 
24
25
  const defaults: Settings = {
@@ -34,6 +35,7 @@ const defaults: Settings = {
34
35
  taskModel: 'default',
35
36
  pipelineModel: 'default',
36
37
  telegramModel: 'sonnet',
38
+ skipPermissions: false,
37
39
  };
38
40
 
39
41
  export function loadSettings(): Settings {
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {