@aion0/forge 0.3.5 → 0.3.6

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
@@ -47,6 +47,21 @@ forge watch <id> # live stream task output
47
47
  - npm package: `@aion0/forge`
48
48
  - GitHub: `github.com/aiwatching/forge`
49
49
 
50
+ ### Help Docs Rule
51
+ When adding or changing a feature, check if `lib/help-docs/` needs updating. Each file covers one module:
52
+ - `00-overview.md` — install, start, data paths
53
+ - `01-settings.md` — all settings fields
54
+ - `02-telegram.md` — bot setup and commands
55
+ - `03-tunnel.md` — remote access
56
+ - `04-tasks.md` — background tasks
57
+ - `05-pipelines.md` — DAG workflows
58
+ - `06-skills.md` — marketplace
59
+ - `07-projects.md` — project management
60
+ - `08-rules.md` — CLAUDE.md templates
61
+ - `09-issue-autofix.md` — GitHub issue scanner
62
+ - `10-troubleshooting.md` — common issues
63
+ If a feature change affects user-facing behavior, update the corresponding help doc in the same commit.
64
+
50
65
  ### Architecture
51
66
  - `forge-server.mjs` starts: Next.js + terminal-standalone + telegram-standalone
52
67
  - `pnpm dev` / `start.sh dev` starts: Next.js (init.ts spawns terminal + telegram)
@@ -0,0 +1,78 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { getConfigDir } from '@/lib/dirs';
5
+ import { loadSettings } from '@/lib/settings';
6
+ import { execSync } from 'node:child_process';
7
+
8
+ const HELP_DIR = join(getConfigDir(), 'help');
9
+ const SOURCE_HELP_DIR = join(process.cwd(), 'lib', 'help-docs');
10
+
11
+ /** Ensure help docs are copied to ~/.forge/help/ */
12
+ function ensureHelpDocs() {
13
+ if (!existsSync(HELP_DIR)) mkdirSync(HELP_DIR, { recursive: true });
14
+ if (existsSync(SOURCE_HELP_DIR)) {
15
+ for (const file of readdirSync(SOURCE_HELP_DIR)) {
16
+ if (!file.endsWith('.md')) continue;
17
+ const src = join(SOURCE_HELP_DIR, file);
18
+ const dest = join(HELP_DIR, file);
19
+ // Always overwrite to keep docs up to date
20
+ writeFileSync(dest, readFileSync(src));
21
+ }
22
+ }
23
+ }
24
+
25
+ /** Check if any agent CLI is available */
26
+ function detectAgent(): { name: string; path: string } | null {
27
+ const settings = loadSettings();
28
+ if (settings.claudePath) {
29
+ try {
30
+ execSync(`"${settings.claudePath}" --version`, { timeout: 5000, stdio: 'pipe' });
31
+ return { name: 'claude', path: settings.claudePath };
32
+ } catch {}
33
+ }
34
+ for (const agent of ['claude', 'codex', 'aider']) {
35
+ try {
36
+ const path = execSync(`which ${agent}`, { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' }).trim();
37
+ if (path) return { name: agent, path };
38
+ } catch {}
39
+ }
40
+ return null;
41
+ }
42
+
43
+ // GET /api/help
44
+ export async function GET(req: Request) {
45
+ const { searchParams } = new URL(req.url);
46
+ const action = searchParams.get('action') || 'status';
47
+
48
+ if (action === 'status') {
49
+ const agent = detectAgent();
50
+ ensureHelpDocs();
51
+ const docs = existsSync(HELP_DIR)
52
+ ? readdirSync(HELP_DIR).filter(f => f.endsWith('.md')).sort()
53
+ : [];
54
+ return NextResponse.json({ agent, docsCount: docs.length, helpDir: HELP_DIR });
55
+ }
56
+
57
+ if (action === 'docs') {
58
+ ensureHelpDocs();
59
+ const docs = existsSync(HELP_DIR)
60
+ ? readdirSync(HELP_DIR).filter(f => f.endsWith('.md')).sort().map(f => ({
61
+ name: f,
62
+ title: f.replace(/^\d+-/, '').replace(/\.md$/, '').replace(/-/g, ' '),
63
+ }))
64
+ : [];
65
+ return NextResponse.json({ docs });
66
+ }
67
+
68
+ if (action === 'doc') {
69
+ const name = searchParams.get('name');
70
+ if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
71
+ ensureHelpDocs();
72
+ const file = join(HELP_DIR, name);
73
+ if (!existsSync(file)) return NextResponse.json({ error: 'Not found' }, { status: 404 });
74
+ return NextResponse.json({ content: readFileSync(file, 'utf-8') });
75
+ }
76
+
77
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
78
+ }
@@ -18,6 +18,7 @@ const CodeViewer = lazy(() => import('./CodeViewer'));
18
18
  const ProjectManager = lazy(() => import('./ProjectManager'));
19
19
  const PreviewPanel = lazy(() => import('./PreviewPanel'));
20
20
  const PipelineView = lazy(() => import('./PipelineView'));
21
+ const HelpDialog = lazy(() => import('./HelpDialog'));
21
22
  const SkillsPanel = lazy(() => import('./SkillsPanel'));
22
23
 
23
24
  interface UsageSummary {
@@ -47,6 +48,7 @@ export default function Dashboard({ user }: { user: any }) {
47
48
  const [showNewTask, setShowNewTask] = useState(false);
48
49
  const [showSettings, setShowSettings] = useState(false);
49
50
  const [showMonitor, setShowMonitor] = useState(false);
51
+ const [showHelp, setShowHelp] = useState(false);
50
52
  const [usage, setUsage] = useState<UsageSummary[]>([]);
51
53
  const [providers, setProviders] = useState<ProviderInfo[]>([]);
52
54
  const [projects, setProjects] = useState<ProjectInfo[]>([]);
@@ -262,6 +264,15 @@ export default function Dashboard({ user }: { user: any }) {
262
264
  + New Task
263
265
  </button>
264
266
  )}
267
+ {/* Help */}
268
+ <button
269
+ onClick={() => setShowHelp(v => !v)}
270
+ className={`text-[10px] px-2 py-0.5 border rounded transition-colors ${
271
+ showHelp
272
+ ? 'border-[var(--accent)] text-[var(--accent)]'
273
+ : 'border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-secondary)]'
274
+ }`}
275
+ >?</button>
265
276
  {/* Preview + Tunnel */}
266
277
  <button
267
278
  onClick={() => setViewMode('preview')}
@@ -597,6 +608,11 @@ export default function Dashboard({ user }: { user: any }) {
597
608
  {showSettings && (
598
609
  <SettingsModal onClose={() => { setShowSettings(false); fetchData(); refreshDisplayName(); }} />
599
610
  )}
611
+ {showHelp && (
612
+ <Suspense fallback={null}>
613
+ <HelpDialog onClose={() => setShowHelp(false)} />
614
+ </Suspense>
615
+ )}
600
616
  </div>
601
617
  );
602
618
  }
@@ -0,0 +1,169 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, lazy, Suspense } from 'react';
4
+
5
+ interface DocItem {
6
+ name: string;
7
+ title: string;
8
+ }
9
+
10
+ const HelpTerminal = lazy(() => import('./HelpTerminal'));
11
+
12
+ export default function HelpDialog({ onClose }: { onClose: () => void }) {
13
+ const [docs, setDocs] = useState<DocItem[]>([]);
14
+ const [agent, setAgent] = useState<{ name: string } | null | undefined>(undefined); // undefined = loading
15
+ const [viewDoc, setViewDoc] = useState<string | null>(null);
16
+ const [docContent, setDocContent] = useState('');
17
+ const [search, setSearch] = useState('');
18
+ const [tab, setTab] = useState<'docs' | 'chat'>('docs');
19
+ const [position, setPosition] = useState({ x: Math.max(0, window.innerWidth - 520), y: 50 });
20
+ const [size, setSize] = useState({ w: 500, h: 560 });
21
+ const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
22
+ const resizeRef = useRef<{ startX: number; startY: number; origW: number; origH: number } | null>(null);
23
+
24
+ useEffect(() => {
25
+ fetch('/api/help?action=status').then(r => r.json())
26
+ .then(data => setAgent(data.agent || null)).catch(() => setAgent(null));
27
+ fetch('/api/help?action=docs').then(r => r.json())
28
+ .then(data => setDocs(data.docs || [])).catch(() => {});
29
+ }, []);
30
+
31
+ const loadDoc = async (name: string) => {
32
+ setViewDoc(name);
33
+ try {
34
+ const res = await fetch(`/api/help?action=doc&name=${encodeURIComponent(name)}`);
35
+ const data = await res.json();
36
+ setDocContent(data.content || '');
37
+ } catch { setDocContent('Failed to load'); }
38
+ };
39
+
40
+ // Drag
41
+ const onDragStart = (e: React.MouseEvent) => {
42
+ e.preventDefault();
43
+ dragRef.current = { startX: e.clientX, startY: e.clientY, origX: position.x, origY: position.y };
44
+ const onMove = (ev: MouseEvent) => {
45
+ if (!dragRef.current) return;
46
+ setPosition({
47
+ x: Math.max(0, dragRef.current.origX + ev.clientX - dragRef.current.startX),
48
+ y: Math.max(0, dragRef.current.origY + ev.clientY - dragRef.current.startY),
49
+ });
50
+ };
51
+ const onUp = () => { dragRef.current = null; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
52
+ window.addEventListener('mousemove', onMove);
53
+ window.addEventListener('mouseup', onUp);
54
+ };
55
+
56
+ // Resize
57
+ const onResizeStart = (e: React.MouseEvent) => {
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+ resizeRef.current = { startX: e.clientX, startY: e.clientY, origW: size.w, origH: size.h };
61
+ const onMove = (ev: MouseEvent) => {
62
+ if (!resizeRef.current) return;
63
+ setSize({
64
+ w: Math.max(350, resizeRef.current.origW + ev.clientX - resizeRef.current.startX),
65
+ h: Math.max(300, resizeRef.current.origH + ev.clientY - resizeRef.current.startY),
66
+ });
67
+ };
68
+ const onUp = () => { resizeRef.current = null; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
69
+ window.addEventListener('mousemove', onMove);
70
+ window.addEventListener('mouseup', onUp);
71
+ };
72
+
73
+ const filtered = search ? docs.filter(d => d.title.toLowerCase().includes(search.toLowerCase())) : docs;
74
+
75
+ return (
76
+ <div
77
+ className="fixed z-50 bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-2xl flex flex-col overflow-hidden"
78
+ style={{ left: position.x, top: position.y, width: size.w, height: size.h }}
79
+ >
80
+ {/* Title bar */}
81
+ <div
82
+ className="flex items-center gap-2 px-3 py-2 bg-[var(--bg-tertiary)] border-b border-[var(--border)] cursor-move shrink-0 select-none"
83
+ onMouseDown={onDragStart}
84
+ >
85
+ <span className="text-[11px] font-semibold text-[var(--text-primary)]">Forge Help</span>
86
+ <div className="ml-auto flex items-center gap-1">
87
+ <button
88
+ onClick={() => { setTab('docs'); setViewDoc(null); }}
89
+ className={`text-[9px] px-2 py-0.5 rounded ${tab === 'docs' ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'text-[var(--text-secondary)]'}`}
90
+ >Docs</button>
91
+ {agent && (
92
+ <button
93
+ onClick={() => setTab('chat')}
94
+ className={`text-[9px] px-2 py-0.5 rounded ${tab === 'chat' ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'text-[var(--text-secondary)]'}`}
95
+ >AI Chat</button>
96
+ )}
97
+ <button onClick={onClose} className="text-[var(--text-secondary)] hover:text-[var(--red)] ml-1 text-sm leading-none">✕</button>
98
+ </div>
99
+ </div>
100
+
101
+ {tab === 'chat' ? (
102
+ /* Embedded terminal */
103
+ <div className="flex-1 min-h-0">
104
+ <Suspense fallback={<div className="flex-1 flex items-center justify-center text-xs text-[var(--text-secondary)]">Loading terminal...</div>}>
105
+ <HelpTerminal />
106
+ </Suspense>
107
+ </div>
108
+ ) : viewDoc ? (
109
+ /* Doc view */
110
+ <>
111
+ <div className="px-3 py-1.5 border-b border-[var(--border)] flex items-center gap-2 shrink-0">
112
+ <button onClick={() => setViewDoc(null)} className="text-[10px] text-[var(--accent)]">← Back</button>
113
+ <span className="text-[10px] text-[var(--text-primary)] font-semibold truncate">
114
+ {docs.find(d => d.name === viewDoc)?.title || viewDoc}
115
+ </span>
116
+ </div>
117
+ <div className="flex-1 overflow-y-auto p-3">
118
+ <pre className="text-[11px] text-[var(--text-primary)] whitespace-pre-wrap break-words font-mono leading-relaxed">
119
+ {docContent}
120
+ </pre>
121
+ </div>
122
+ </>
123
+ ) : (
124
+ /* Doc list */
125
+ <>
126
+ <div className="px-3 py-2 border-b border-[var(--border)] shrink-0">
127
+ <input
128
+ type="text"
129
+ value={search}
130
+ onChange={e => setSearch(e.target.value)}
131
+ placeholder="Search help topics..."
132
+ className="w-full px-2 py-1 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-[10px] text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
133
+ autoFocus
134
+ />
135
+ </div>
136
+ {!agent && agent !== undefined && (
137
+ <div className="px-3 py-2 bg-[var(--yellow)]/10 border-b border-[var(--border)] shrink-0">
138
+ <p className="text-[9px] text-[var(--text-secondary)]">
139
+ Install Claude Code for AI help: <code className="text-[var(--accent)]">npm i -g @anthropic-ai/claude-code</code>
140
+ </p>
141
+ </div>
142
+ )}
143
+ <div className="flex-1 overflow-y-auto">
144
+ {filtered.map(doc => (
145
+ <button
146
+ key={doc.name}
147
+ onClick={() => loadDoc(doc.name)}
148
+ className="w-full text-left px-3 py-2.5 border-b border-[var(--border)]/30 hover:bg-[var(--bg-tertiary)] text-[11px] text-[var(--text-primary)] capitalize"
149
+ >
150
+ {doc.title}
151
+ </button>
152
+ ))}
153
+ </div>
154
+ <div className="px-3 py-2 border-t border-[var(--border)] shrink-0">
155
+ <a href="https://github.com/aiwatching/forge" target="_blank" rel="noopener noreferrer"
156
+ className="text-[9px] text-[var(--text-secondary)] hover:text-[var(--accent)]">GitHub →</a>
157
+ </div>
158
+ </>
159
+ )}
160
+
161
+ {/* Resize handle */}
162
+ <div
163
+ onMouseDown={onResizeStart}
164
+ className="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize"
165
+ style={{ background: 'linear-gradient(135deg, transparent 50%, var(--border) 50%)' }}
166
+ />
167
+ </div>
168
+ );
169
+ }
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { Terminal } from '@xterm/xterm';
5
+ import { FitAddon } from '@xterm/addon-fit';
6
+ import '@xterm/xterm/css/xterm.css';
7
+
8
+ const SESSION_NAME = 'mw-forge-help';
9
+
10
+ function getWsUrl() {
11
+ if (typeof window === 'undefined') return `ws://localhost:${parseInt(process.env.TERMINAL_PORT || '3001')}`;
12
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
13
+ const wsHost = window.location.hostname;
14
+ if (wsHost !== 'localhost' && wsHost !== '127.0.0.1') {
15
+ return `${wsProtocol}//${window.location.host}/terminal-ws`;
16
+ }
17
+ const webPort = parseInt(window.location.port) || 3000;
18
+ return `${wsProtocol}//${wsHost}:${webPort + 1}`;
19
+ }
20
+
21
+ export default function HelpTerminal() {
22
+ const containerRef = useRef<HTMLDivElement>(null);
23
+ const [connected, setConnected] = useState(false);
24
+
25
+ useEffect(() => {
26
+ if (!containerRef.current) return;
27
+
28
+ let disposed = false;
29
+ const cs = getComputedStyle(document.documentElement);
30
+ const tv = (name: string) => cs.getPropertyValue(name).trim();
31
+ const term = new Terminal({
32
+ cursorBlink: true,
33
+ fontSize: 12,
34
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
35
+ scrollback: 3000,
36
+ logger: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
37
+ theme: {
38
+ background: tv('--term-bg') || '#1a1a2e',
39
+ foreground: tv('--term-fg') || '#e0e0e0',
40
+ cursor: tv('--term-cursor') || '#7c5bf0',
41
+ selectionBackground: (tv('--term-cursor') || '#7c5bf0') + '44',
42
+ },
43
+ });
44
+ const fit = new FitAddon();
45
+ term.loadAddon(fit);
46
+ term.open(containerRef.current);
47
+ try { fit.fit(); } catch {}
48
+
49
+ const wsUrl = getWsUrl();
50
+ let ws: WebSocket | null = null;
51
+ let reconnectTimer = 0;
52
+ let isNewSession = false;
53
+
54
+ function connect() {
55
+ if (disposed) return;
56
+ const socket = new WebSocket(wsUrl);
57
+ ws = socket;
58
+
59
+ socket.onopen = () => {
60
+ if (disposed) { socket.close(); return; }
61
+ socket.send(JSON.stringify({ type: 'attach', sessionName: SESSION_NAME, cols: term.cols, rows: term.rows }));
62
+ };
63
+
64
+ socket.onmessage = (event) => {
65
+ if (disposed) return;
66
+ try {
67
+ const msg = JSON.parse(event.data);
68
+ if (msg.type === 'output') {
69
+ try { term.write(msg.data); } catch {}
70
+ } else if (msg.type === 'connected') {
71
+ setConnected(true);
72
+ if (isNewSession) {
73
+ isNewSession = false;
74
+ setTimeout(() => {
75
+ if (socket.readyState === WebSocket.OPEN) {
76
+ socket.send(JSON.stringify({ type: 'input', data: `cd ~/.forge/help 2>/dev/null && claude\n` }));
77
+ }
78
+ }, 300);
79
+ }
80
+ } else if (msg.type === 'error') {
81
+ isNewSession = true;
82
+ if (socket.readyState === WebSocket.OPEN) {
83
+ socket.send(JSON.stringify({ type: 'create', cols: term.cols, rows: term.rows, sessionName: SESSION_NAME }));
84
+ }
85
+ }
86
+ } catch {}
87
+ };
88
+
89
+ socket.onclose = () => {
90
+ if (disposed) return;
91
+ setConnected(false);
92
+ reconnectTimer = window.setTimeout(connect, 3000);
93
+ };
94
+ socket.onerror = () => {};
95
+ }
96
+
97
+ connect();
98
+
99
+ term.onData((data) => {
100
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data }));
101
+ });
102
+
103
+ const resizeObserver = new ResizeObserver(() => {
104
+ const el = containerRef.current;
105
+ if (!el || el.offsetWidth < 50 || el.offsetHeight < 30) return;
106
+ try {
107
+ fit.fit();
108
+ if (term.cols < 2 || term.rows < 2) return;
109
+ if (ws?.readyState === WebSocket.OPEN) {
110
+ ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
111
+ }
112
+ } catch {}
113
+ });
114
+ resizeObserver.observe(containerRef.current);
115
+
116
+ return () => {
117
+ disposed = true;
118
+ clearTimeout(reconnectTimer);
119
+ ws?.close();
120
+ resizeObserver.disconnect();
121
+ term.dispose();
122
+ };
123
+ }, []);
124
+
125
+ return (
126
+ <div className="h-full flex flex-col">
127
+ <div ref={containerRef} className="flex-1" />
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,34 @@
1
+ # Forge Overview
2
+
3
+ Forge is a self-hosted Vibe Coding platform for Claude Code. It provides a browser-based terminal, AI task orchestration, remote access, and mobile control via Telegram.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install -g @aion0/forge
9
+ forge server start
10
+ ```
11
+
12
+ Open `http://localhost:3000`. First launch prompts you to set an admin password.
13
+
14
+ ## Requirements
15
+ - Node.js >= 20
16
+ - tmux (`brew install tmux` on macOS)
17
+ - Claude Code CLI (`npm install -g @anthropic-ai/claude-code`)
18
+
19
+ ## Data Location
20
+ - Config: `~/.forge/` (binaries)
21
+ - Data: `~/.forge/data/` (settings, database, state)
22
+ - Claude: `~/.claude/` (skills, commands, sessions)
23
+
24
+ ## Server Commands
25
+ ```bash
26
+ forge server start # background (default)
27
+ forge server start --foreground # foreground
28
+ forge server start --dev # dev mode with hot-reload
29
+ forge server stop # stop
30
+ forge server restart # restart
31
+ forge server start --port 4000 # custom port
32
+ forge server start --dir ~/.forge-test # custom data dir
33
+ forge --reset-password # reset admin password
34
+ ```
@@ -0,0 +1,37 @@
1
+ # Settings Configuration
2
+
3
+ Settings are stored in `~/.forge/data/settings.yaml`. Configure via the web UI (Settings button in top-right menu) or edit YAML directly.
4
+
5
+ ## All Settings Fields
6
+
7
+ | Field | Type | Default | Description |
8
+ |-------|------|---------|-------------|
9
+ | `projectRoots` | string[] | `[]` | Directories containing your projects (e.g. `~/Projects`) |
10
+ | `docRoots` | string[] | `[]` | Markdown/Obsidian vault directories |
11
+ | `claudePath` | string | `""` | Path to claude binary (auto-detected if empty) |
12
+ | `claudeHome` | string | `""` | Claude Code home directory (default: `~/.claude`) |
13
+ | `telegramBotToken` | string | `""` | Telegram Bot API token (encrypted) |
14
+ | `telegramChatId` | string | `""` | Telegram chat ID (comma-separated for multiple users) |
15
+ | `notifyOnComplete` | boolean | `true` | Telegram notification on task completion |
16
+ | `notifyOnFailure` | boolean | `true` | Telegram notification on task failure |
17
+ | `tunnelAutoStart` | boolean | `false` | Auto-start Cloudflare Tunnel on server startup |
18
+ | `telegramTunnelPassword` | string | `""` | Admin password for login + tunnel + secrets (encrypted) |
19
+ | `taskModel` | string | `"default"` | Model for background tasks |
20
+ | `pipelineModel` | string | `"default"` | Model for pipeline workflows |
21
+ | `telegramModel` | string | `"sonnet"` | Model for Telegram AI features |
22
+ | `skipPermissions` | boolean | `false` | Add `--dangerously-skip-permissions` to claude invocations |
23
+ | `notificationRetentionDays` | number | `30` | Auto-cleanup notifications older than N days |
24
+ | `skillsRepoUrl` | string | forge-skills URL | GitHub raw URL for skills registry |
25
+ | `displayName` | string | `"Forge"` | Display name shown in header |
26
+ | `displayEmail` | string | `""` | User email |
27
+
28
+ ## Admin Password
29
+
30
+ - Set on first launch (CLI prompt)
31
+ - Required for: login, tunnel start, secret changes, Telegram commands
32
+ - Reset: `forge --reset-password`
33
+ - Forgot? Run `forge --reset-password` in terminal
34
+
35
+ ## Encrypted Fields
36
+
37
+ `telegramBotToken` and `telegramTunnelPassword` are encrypted with AES-256-GCM. The encryption key is stored at `~/.forge/data/.encrypt-key`.
@@ -0,0 +1,41 @@
1
+ # Telegram Bot Setup
2
+
3
+ ## Setup Steps
4
+
5
+ 1. Open Telegram, search for [@BotFather](https://t.me/botfather)
6
+ 2. Send `/newbot`, follow prompts to create a bot
7
+ 3. Copy the bot token (looks like `6234567890:ABCDefGHIJKLMNOPQRSTUVWXYZ`)
8
+ 4. In Forge Settings, paste the token into **Telegram Bot Token**
9
+ 5. To get your Chat ID: send any message to your bot, then visit `https://api.telegram.org/bot<TOKEN>/getUpdates` — find `chat.id` in the response
10
+ 6. Paste the Chat ID into **Telegram Chat ID** in Settings
11
+ 7. The bot starts automatically after saving
12
+
13
+ ## Commands
14
+
15
+ | Command | Description |
16
+ |---------|-------------|
17
+ | `/task <project> <prompt>` | Create a background task |
18
+ | `/tasks [status]` | List tasks (running/queued/done/failed) |
19
+ | `/sessions [project]` | AI summary of Claude Code sessions |
20
+ | `/watch <id>` | Live stream task output |
21
+ | `/unwatch <id>` | Stop streaming |
22
+ | `/docs <query>` | Search Obsidian vault |
23
+ | `/note <text>` | Quick note to vault |
24
+ | `/peek <project>` | Preview running session |
25
+ | `/cancel <id>` | Cancel a task |
26
+ | `/retry <id>` | Retry a failed task |
27
+ | `/tunnel_start <password>` | Start Cloudflare Tunnel (returns URL + code) |
28
+ | `/tunnel_stop` | Stop tunnel |
29
+ | `/tunnel_code <password>` | Get session code for remote login |
30
+ | `/projects` | List configured projects |
31
+
32
+ ## Shortcuts
33
+ - Reply to a task message to interact with it
34
+ - Send `"project: instructions"` to quick-create a task
35
+ - Numbered lists — reply with a number to select
36
+
37
+ ## Troubleshooting
38
+
39
+ - **Bot not responding**: Check token is correct, restart server
40
+ - **"Unauthorized"**: Chat ID doesn't match configured value
41
+ - **Multiple users**: Set comma-separated Chat IDs (e.g. `123456,789012`)
@@ -0,0 +1,31 @@
1
+ # Remote Access (Cloudflare Tunnel)
2
+
3
+ ## How It Works
4
+
5
+ Forge creates a temporary Cloudflare Tunnel — a secure public URL that routes to your local Forge server. No Cloudflare account needed.
6
+
7
+ ## Start Tunnel
8
+
9
+ **From UI**: Click the "Tunnel" button in the top-right header.
10
+
11
+ **From Telegram**: `/tunnel_start <admin_password>`
12
+
13
+ **Auto-start**: Set `tunnelAutoStart: true` in Settings.
14
+
15
+ ## Login Flow
16
+
17
+ - **Local access** (localhost, LAN): Admin password only
18
+ - **Remote access** (via tunnel, `.trycloudflare.com`): Admin password + Session Code (2FA)
19
+
20
+ Session code is generated when tunnel starts. Get it via:
21
+ - Telegram: `/tunnel_code <password>`
22
+ - CLI: `forge tcode`
23
+
24
+ ## Troubleshooting
25
+
26
+ - **Tunnel stuck at "starting"**: Kill old cloudflared processes: `pkill -f cloudflared`
27
+ - **URL not reachable**: Tunnel may have timed out, restart it
28
+ - **Session cookie invalid after restart**: Set `AUTH_SECRET` in `~/.forge/data/.env.local`:
29
+ ```bash
30
+ echo "AUTH_SECRET=$(openssl rand -hex 32)" >> ~/.forge/data/.env.local
31
+ ```
@@ -0,0 +1,52 @@
1
+ # Background Tasks
2
+
3
+ ## What Are Tasks?
4
+
5
+ Tasks run Claude Code prompts in the background. They use `claude -p` (print mode) — execute and exit, no persistent session. Your code runs on your machine using your Claude subscription.
6
+
7
+ ## Create a Task
8
+
9
+ **From UI**: Click "+ New Task" in the Tasks tab.
10
+
11
+ **From CLI**:
12
+ ```bash
13
+ forge task my-project "fix the login bug"
14
+ forge task my-project "add unit tests for utils.ts" --new # fresh session
15
+ ```
16
+
17
+ **From Telegram**: `/task my-project fix the login bug`
18
+
19
+ ## Task Modes
20
+
21
+ | Mode | Description |
22
+ |------|-------------|
23
+ | `prompt` | Run Claude Code with a prompt (default) |
24
+ | `shell` | Execute raw shell command |
25
+ | `monitor` | Watch a session and trigger actions |
26
+
27
+ ## Watch Task Output
28
+
29
+ ```bash
30
+ forge watch <task-id> # live stream in terminal
31
+ ```
32
+
33
+ Or from Telegram: `/watch <task-id>`
34
+
35
+ ## CLI Commands
36
+
37
+ ```bash
38
+ forge tasks # list all tasks
39
+ forge tasks running # filter by status
40
+ forge status <id> # task details
41
+ forge cancel <id> # cancel
42
+ forge retry <id> # retry failed task
43
+ forge log <id> # execution log
44
+ ```
45
+
46
+ ## Features
47
+
48
+ - **Per-project concurrency**: One prompt task per project at a time, others queue
49
+ - **Session continuity**: All tasks in the same project share one Claude conversation
50
+ - **Cost tracking**: Token usage and USD cost per task
51
+ - **Git tracking**: Captures branch name and git diff after execution
52
+ - **Scheduled execution**: Set `scheduledAt` for deferred tasks
@@ -0,0 +1,73 @@
1
+ # Pipelines (Workflows)
2
+
3
+ ## What Are Pipelines?
4
+
5
+ Pipelines chain multiple tasks into a DAG (directed acyclic graph). Each step can depend on previous steps, pass outputs forward, and run in parallel.
6
+
7
+ ## YAML Workflow Format
8
+
9
+ ```yaml
10
+ name: my-workflow
11
+ description: "What this workflow does"
12
+ input:
13
+ feature: "Feature description"
14
+ vars:
15
+ project: my-app
16
+ nodes:
17
+ design:
18
+ project: "{{vars.project}}"
19
+ prompt: "Design: {{input.feature}}"
20
+ outputs:
21
+ - name: spec
22
+ extract: result
23
+ implement:
24
+ project: "{{vars.project}}"
25
+ depends_on: [design]
26
+ prompt: "Implement: {{nodes.design.outputs.spec}}"
27
+ review:
28
+ project: "{{vars.project}}"
29
+ depends_on: [implement]
30
+ prompt: "Review the changes"
31
+ ```
32
+
33
+ ## Node Options
34
+
35
+ | Field | Description |
36
+ |-------|-------------|
37
+ | `project` | Project name (supports `{{vars.xxx}}` templates) |
38
+ | `prompt` | Claude Code prompt or shell command |
39
+ | `mode` | `claude` (default) or `shell` |
40
+ | `branch` | Auto-checkout branch before running |
41
+ | `depends_on` | List of node IDs that must complete first |
42
+ | `outputs` | Extract results: `result`, `git_diff`, or `stdout` |
43
+ | `routes` | Conditional routing to next nodes |
44
+
45
+ ## Template Variables
46
+
47
+ - `{{input.xxx}}` — pipeline input values
48
+ - `{{vars.xxx}}` — workflow variables
49
+ - `{{nodes.xxx.outputs.yyy}}` — outputs from previous nodes
50
+
51
+ ## Built-in Workflows
52
+
53
+ ### issue-auto-fix
54
+ Fetches a GitHub issue → fixes code on new branch → creates PR.
55
+
56
+ Input: `issue_id`, `project`, `base_branch` (optional)
57
+
58
+ ### pr-review
59
+ Fetches PR diff → AI code review → posts result.
60
+
61
+ Input: `pr_number`, `project`
62
+
63
+ ## CLI
64
+
65
+ ```bash
66
+ forge flows # list available workflows
67
+ forge run my-workflow # execute a workflow
68
+ ```
69
+
70
+ ## Storage
71
+
72
+ - Workflow YAML: `~/.forge/data/flows/`
73
+ - Execution state: `~/.forge/data/pipelines/`
@@ -0,0 +1,43 @@
1
+ # Skills Marketplace
2
+
3
+ ## Overview
4
+
5
+ Browse, install, and manage Claude Code skills and commands from the Forge Skills registry.
6
+
7
+ ## Types
8
+
9
+ | | Skills | Commands |
10
+ |---|---|---|
11
+ | Location | `~/.claude/skills/<name>/` | `~/.claude/commands/<name>.md` |
12
+ | Entry file | `SKILL.md` | Single `.md` file |
13
+ | Complexity | Multi-file with templates | Simple slash command |
14
+
15
+ Both register as `/slash-command` in Claude Code.
16
+
17
+ ## Install
18
+
19
+ 1. Go to **Skills** tab in Forge
20
+ 2. Click **Sync** to fetch latest registry
21
+ 3. Click **Install** on any skill → choose Global or specific project
22
+ 4. Use in Claude Code with `/<skill-name>`
23
+
24
+ ## Update
25
+
26
+ Skills with newer versions show a yellow "update" indicator. Click to update (checks for local modifications first).
27
+
28
+ ## Local Skills
29
+
30
+ The **Local** tab shows skills/commands installed on your machine (both from marketplace and manually created). You can:
31
+ - **Install to...** — Copy a local skill to another project or global
32
+ - **Delete** — Remove from project or global
33
+ - **Edit** — View and modify installed files
34
+
35
+ ## Registry
36
+
37
+ Default: `https://raw.githubusercontent.com/aiwatching/forge-skills/main`
38
+
39
+ Change in Settings → Skills Repo URL.
40
+
41
+ ## Custom Skills
42
+
43
+ Create your own: put a `.md` file in `<project>/.claude/commands/` or a directory in `<project>/.claude/skills/<name>/`.
@@ -0,0 +1,39 @@
1
+ # Projects
2
+
3
+ ## Setup
4
+
5
+ Add project directories in Settings → **Project Roots** (e.g. `~/Projects`). Forge scans subdirectories automatically.
6
+
7
+ ## Features
8
+
9
+ ### Code Tab
10
+ - File tree browser
11
+ - Syntax-highlighted code viewer
12
+ - Git diff view (click changed files)
13
+ - Git operations: commit, push, pull
14
+ - Commit history
15
+
16
+ ### Skills & Commands Tab
17
+ - View installed skills/commands for this project
18
+ - Scope indicator: G (global), P (project), G+P (both)
19
+ - Edit files, update from marketplace, uninstall
20
+
21
+ ### CLAUDE.md Tab
22
+ - View and edit project's CLAUDE.md
23
+ - Apply rule templates (built-in or custom)
24
+ - Templates auto-injected with dedup markers
25
+
26
+ ### Issues Tab
27
+ - Enable GitHub Issue Auto-fix per project
28
+ - Configure scan interval and label filters
29
+ - Manual trigger: enter issue # and click Fix Issue
30
+ - Processed issues history with retry/delete
31
+ - Auto-chains: fix → create PR → review
32
+
33
+ ## Favorites
34
+
35
+ Click ★ next to a project to favorite it. Favorites appear at the top of the sidebar.
36
+
37
+ ## Terminal
38
+
39
+ Click "Terminal" button in project header to open a Vibe Coding terminal for that project. Uses `claude -c` to continue last session.
@@ -0,0 +1,53 @@
1
+ # Rules (CLAUDE.md Templates)
2
+
3
+ ## What Are Rules?
4
+
5
+ Reusable markdown snippets that get appended to project CLAUDE.md files. They define coding conventions, security rules, workflow guidelines, etc.
6
+
7
+ ## Built-in Templates
8
+
9
+ | Template | Description |
10
+ |----------|-------------|
11
+ | TypeScript Rules | Coding conventions (const, types, early returns) |
12
+ | Git Workflow | Commit messages, branch naming |
13
+ | Obsidian Vault | Vault integration instructions |
14
+ | Security Rules | OWASP guidelines, no hardcoded secrets |
15
+
16
+ ## Manage Rules
17
+
18
+ **Skills tab → Rules sub-tab**:
19
+ - View all templates (built-in + custom)
20
+ - Create new: click "+ New"
21
+ - Edit any template (including built-in)
22
+ - Delete custom templates
23
+ - Set as "default" — auto-applied to new projects
24
+ - Batch apply: select template → check projects → click "Apply"
25
+
26
+ ## Apply to Project
27
+
28
+ **Project → CLAUDE.md tab**:
29
+ - Left sidebar shows CLAUDE.md content + template list
30
+ - Click "+ add" to inject a template
31
+ - Click "added" to remove
32
+ - Templates wrapped in `<!-- forge:template:id -->` markers (prevents duplicate injection)
33
+
34
+ ## Default Templates
35
+
36
+ Templates marked as "default" are automatically injected into new projects when they first appear in the project list.
37
+
38
+ ## Custom Templates
39
+
40
+ Stored in `~/.forge/data/claude-templates/`. Each is a `.md` file with YAML frontmatter:
41
+
42
+ ```markdown
43
+ ---
44
+ name: My Rule
45
+ description: What this rule does
46
+ tags: [category]
47
+ builtin: false
48
+ isDefault: false
49
+ ---
50
+
51
+ ## My Custom Rule
52
+ Your content here...
53
+ ```
@@ -0,0 +1,51 @@
1
+ # Issue Auto-fix
2
+
3
+ ## Overview
4
+
5
+ Automatically scan GitHub Issues, fix code, create PRs, and review — all hands-free.
6
+
7
+ ## Prerequisites
8
+
9
+ - `gh` CLI installed and authenticated: `gh auth login`
10
+ - Project has a GitHub remote
11
+
12
+ ## Setup
13
+
14
+ 1. Go to **Projects → select project → Issues tab**
15
+ 2. Enable **Issue Auto-fix**
16
+ 3. Configure:
17
+ - **Scan Interval**: minutes between scans (0 = manual only)
18
+ - **Base Branch**: leave empty for auto-detect (main/master)
19
+ - **Labels Filter**: comma-separated labels (empty = all issues)
20
+ 4. Click **Scan Now** to test
21
+
22
+ ## Flow
23
+
24
+ ```
25
+ Scan → Fetch Issue → Fix Code (new branch) → Push → Create PR → Auto Review → Notify
26
+ ```
27
+
28
+ 1. **Scan**: `gh issue list` finds open issues matching labels
29
+ 2. **Fix**: Claude Code analyzes issue and fixes code on `fix/<id>-<description>` branch
30
+ 3. **PR**: Pushes branch and creates Pull Request
31
+ 4. **Review**: Automatically triggers `pr-review` pipeline
32
+ 5. **Notify**: Results sent via Telegram (if configured)
33
+
34
+ ## Manual Trigger
35
+
36
+ Enter an issue number in "Manual Trigger" section and click "Fix Issue".
37
+
38
+ ## Retry
39
+
40
+ Failed fixes show a "Retry" button. Click to provide additional context (e.g. "rebase from main first") and re-run.
41
+
42
+ ## Safety
43
+
44
+ - Checks for uncommitted changes before starting (aborts if dirty)
45
+ - Always works on new branches (never modifies main)
46
+ - Switches back to original branch after completion
47
+ - Existing PRs are updated, not duplicated
48
+
49
+ ## Processed Issues
50
+
51
+ History shows all processed issues with status (processing/done/failed), PR number, and pipeline ID. Click pipeline ID to view details.
@@ -0,0 +1,82 @@
1
+ # Troubleshooting
2
+
3
+ ## Common Issues
4
+
5
+ ### "fork failed: Device not configured" (macOS)
6
+ PTY device limit exhausted:
7
+ ```bash
8
+ sudo sysctl kern.tty.ptmx_max=2048
9
+ echo 'kern.tty.ptmx_max=2048' | sudo tee -a /etc/sysctl.conf
10
+ ```
11
+
12
+ ### Session cookie invalid after restart
13
+ Fix AUTH_SECRET so it persists:
14
+ ```bash
15
+ echo "AUTH_SECRET=$(openssl rand -hex 32)" >> ~/.forge/data/.env.local
16
+ ```
17
+
18
+ ### Orphan processes after Ctrl+C
19
+ Use `forge server stop` or:
20
+ ```bash
21
+ pkill -f 'telegram-standalone|terminal-standalone|next-server|cloudflared'
22
+ ```
23
+
24
+ ### Tunnel stuck at "starting"
25
+ ```bash
26
+ pkill -f cloudflared
27
+ # Then restart tunnel from UI or Telegram
28
+ ```
29
+
30
+ ### Forgot admin password
31
+ ```bash
32
+ forge --reset-password
33
+ ```
34
+
35
+ ### Terminal tabs lost after restart
36
+ Terminal state is saved in `~/.forge/data/terminal-state.json`. If corrupted:
37
+ ```bash
38
+ rm ~/.forge/data/terminal-state.json
39
+ # Restart server — tabs will be empty but tmux sessions survive
40
+ ```
41
+
42
+ ### gh CLI not authenticated (Issue Scanner)
43
+ ```bash
44
+ gh auth login
45
+ ```
46
+
47
+ ### Skills not syncing
48
+ Click "Sync" in Skills tab. Check `skillsRepoUrl` in Settings points to valid registry.
49
+
50
+ ### Multiple instances conflict
51
+ Use different ports and data directories:
52
+ ```bash
53
+ forge server start --port 4000 --dir ~/.forge/data_demo
54
+ forge server stop --port 4000 --dir ~/.forge/data_demo
55
+ ```
56
+
57
+ ### Page shows "Failed to load chunk"
58
+ Clear build cache:
59
+ ```bash
60
+ rm -rf .next
61
+ pnpm build # or forge server rebuild
62
+ ```
63
+
64
+ ## Logs
65
+
66
+ - Background server: `~/.forge/data/forge.log`
67
+ - Dev mode: terminal output
68
+ - View with: `tail -f ~/.forge/data/forge.log`
69
+
70
+ ## Reset Everything
71
+
72
+ ```bash
73
+ # Stop server
74
+ forge server stop
75
+
76
+ # Reset password
77
+ forge --reset-password
78
+
79
+ # Clear all data (nuclear option)
80
+ rm -rf ~/.forge/data
81
+ # Restart — will create fresh data directory
82
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {