@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 +15 -0
- package/app/api/help/route.ts +78 -0
- package/components/Dashboard.tsx +16 -0
- package/components/HelpDialog.tsx +169 -0
- package/components/HelpTerminal.tsx +130 -0
- package/lib/help-docs/00-overview.md +34 -0
- package/lib/help-docs/01-settings.md +37 -0
- package/lib/help-docs/02-telegram.md +41 -0
- package/lib/help-docs/03-tunnel.md +31 -0
- package/lib/help-docs/04-tasks.md +52 -0
- package/lib/help-docs/05-pipelines.md +73 -0
- package/lib/help-docs/06-skills.md +43 -0
- package/lib/help-docs/07-projects.md +39 -0
- package/lib/help-docs/08-rules.md +53 -0
- package/lib/help-docs/09-issue-autofix.md +51 -0
- package/lib/help-docs/10-troubleshooting.md +82 -0
- package/package.json +1 -1
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
|
+
}
|
package/components/Dashboard.tsx
CHANGED
|
@@ -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
|
+
```
|