@aion0/forge 0.4.16 → 0.5.1
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/README.md +27 -2
- package/RELEASE_NOTES.md +21 -14
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +13 -1
- package/check-forge-status.sh +9 -0
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +10 -2
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +11 -6
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +31 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +257 -43
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2245 -0
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +7 -1
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +89 -0
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +254 -0
- package/lib/help-docs/CLAUDE.md +7 -2
- package/lib/init.ts +60 -10
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1914 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +814 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +4 -1
- package/src/config/index.ts +12 -1
- package/src/core/db/database.ts +1 -0
- package/start.sh +7 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { signIn } from 'next-auth/react';
|
|
5
|
+
|
|
6
|
+
export default function LoginForm({ isRemote }: { isRemote: boolean }) {
|
|
7
|
+
const [password, setPassword] = useState('');
|
|
8
|
+
const [sessionCode, setSessionCode] = useState('');
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const saved = localStorage.getItem('forge-theme');
|
|
14
|
+
if (saved === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setError('');
|
|
20
|
+
const result = await signIn('credentials', {
|
|
21
|
+
password,
|
|
22
|
+
sessionCode: isRemote ? sessionCode : '',
|
|
23
|
+
isRemote: String(isRemote),
|
|
24
|
+
redirect: false,
|
|
25
|
+
}) as { error?: string; ok?: boolean } | undefined;
|
|
26
|
+
if (result?.error) {
|
|
27
|
+
setError(isRemote ? 'Wrong password or session code' : 'Wrong password');
|
|
28
|
+
} else if (result?.ok) {
|
|
29
|
+
window.location.href = window.location.origin + '/';
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
35
|
+
<div className="w-80 space-y-6">
|
|
36
|
+
<div className="text-center">
|
|
37
|
+
<img src="/icon.png" alt="Forge" width={48} height={48} className="rounded mx-auto mb-2" />
|
|
38
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
|
|
39
|
+
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
40
|
+
{isRemote ? 'Remote Access' : 'Local Access'}
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
45
|
+
<input
|
|
46
|
+
type="password"
|
|
47
|
+
value={password}
|
|
48
|
+
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
49
|
+
placeholder="Admin Password"
|
|
50
|
+
autoFocus
|
|
51
|
+
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
52
|
+
/>
|
|
53
|
+
{isRemote && (
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
inputMode="numeric"
|
|
57
|
+
pattern="[0-9]*"
|
|
58
|
+
maxLength={8}
|
|
59
|
+
value={sessionCode}
|
|
60
|
+
onChange={e => { setSessionCode(e.target.value.replace(/\D/g, '')); setError(''); }}
|
|
61
|
+
placeholder="Session Code (8 digits)"
|
|
62
|
+
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] font-mono tracking-widest text-center focus:outline-none focus:border-[var(--accent)]"
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
{error && <p className="text-xs text-[var(--red)]">{error}</p>}
|
|
66
|
+
<button
|
|
67
|
+
type="submit"
|
|
68
|
+
className="w-full py-2 bg-[var(--accent)] text-white rounded text-sm hover:opacity-90"
|
|
69
|
+
>
|
|
70
|
+
Sign In
|
|
71
|
+
</button>
|
|
72
|
+
{isRemote && (
|
|
73
|
+
<p className="text-[10px] text-[var(--text-secondary)] text-center">
|
|
74
|
+
Session code is generated when tunnel starts. Use /tunnel_code in Telegram or <code>forge tcode</code> to get it.
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
<div className="text-center">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => setShowHelp(v => !v)}
|
|
81
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
82
|
+
>
|
|
83
|
+
Forgot password?
|
|
84
|
+
</button>
|
|
85
|
+
{showHelp && (
|
|
86
|
+
<p className="text-[10px] text-[var(--text-secondary)] mt-1 bg-[var(--bg-tertiary)] rounded p-2">
|
|
87
|
+
Run in terminal:<br />
|
|
88
|
+
<code className="text-[var(--accent)]">forge --reset-password</code>
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</form>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/app/login/page.tsx
CHANGED
|
@@ -1,101 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { headers } from 'next/headers';
|
|
2
|
+
import LoginForm from './LoginForm';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
export default async function LoginPage() {
|
|
5
|
+
const headersList = await headers();
|
|
6
|
+
const host = headersList.get('host') || '';
|
|
7
|
+
const isRemote = host.endsWith('.trycloudflare.com');
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
const [password, setPassword] = useState('');
|
|
8
|
-
const [sessionCode, setSessionCode] = useState('');
|
|
9
|
-
const [error, setError] = useState('');
|
|
10
|
-
const [isRemote, setIsRemote] = useState(false);
|
|
11
|
-
const [showHelp, setShowHelp] = useState(false);
|
|
12
|
-
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const host = window.location.hostname;
|
|
15
|
-
setIsRemote(host.endsWith('.trycloudflare.com'));
|
|
16
|
-
// Restore theme
|
|
17
|
-
const saved = localStorage.getItem('forge-theme');
|
|
18
|
-
if (saved === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
const handleLocal = async (e: React.FormEvent) => {
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
setError('');
|
|
24
|
-
const result = await signIn('credentials', {
|
|
25
|
-
password,
|
|
26
|
-
sessionCode: isRemote ? sessionCode : '',
|
|
27
|
-
isRemote: String(isRemote),
|
|
28
|
-
redirect: false,
|
|
29
|
-
}) as { error?: string; ok?: boolean } | undefined;
|
|
30
|
-
if (result?.error) {
|
|
31
|
-
setError(isRemote ? 'Wrong password or session code' : 'Wrong password');
|
|
32
|
-
} else if (result?.ok) {
|
|
33
|
-
window.location.href = window.location.origin + '/';
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
39
|
-
<div className="w-80 space-y-6">
|
|
40
|
-
<div className="text-center">
|
|
41
|
-
<img src="/icon.png" alt="Forge" width={48} height={48} className="rounded mx-auto mb-2" />
|
|
42
|
-
<h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
|
|
43
|
-
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
44
|
-
{isRemote ? 'Remote Access' : 'Local Access'}
|
|
45
|
-
</p>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<form onSubmit={handleLocal} className="space-y-3">
|
|
49
|
-
<input
|
|
50
|
-
type="password"
|
|
51
|
-
value={password}
|
|
52
|
-
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
53
|
-
placeholder="Admin Password"
|
|
54
|
-
autoFocus
|
|
55
|
-
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
56
|
-
/>
|
|
57
|
-
{isRemote && (
|
|
58
|
-
<input
|
|
59
|
-
type="text"
|
|
60
|
-
inputMode="numeric"
|
|
61
|
-
pattern="[0-9]*"
|
|
62
|
-
maxLength={8}
|
|
63
|
-
value={sessionCode}
|
|
64
|
-
onChange={e => { setSessionCode(e.target.value.replace(/\D/g, '')); setError(''); }}
|
|
65
|
-
placeholder="Session Code (8 digits)"
|
|
66
|
-
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] font-mono tracking-widest text-center focus:outline-none focus:border-[var(--accent)]"
|
|
67
|
-
/>
|
|
68
|
-
)}
|
|
69
|
-
{error && <p className="text-xs text-[var(--red)]">{error}</p>}
|
|
70
|
-
<button
|
|
71
|
-
type="submit"
|
|
72
|
-
className="w-full py-2 bg-[var(--accent)] text-white rounded text-sm hover:opacity-90"
|
|
73
|
-
>
|
|
74
|
-
Sign In
|
|
75
|
-
</button>
|
|
76
|
-
{isRemote && (
|
|
77
|
-
<p className="text-[10px] text-[var(--text-secondary)] text-center">
|
|
78
|
-
Session code is generated when tunnel starts. Use /tunnel_code in Telegram to get it.
|
|
79
|
-
</p>
|
|
80
|
-
)}
|
|
81
|
-
<div className="text-center">
|
|
82
|
-
<button
|
|
83
|
-
type="button"
|
|
84
|
-
onClick={() => setShowHelp(v => !v)}
|
|
85
|
-
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
86
|
-
>
|
|
87
|
-
Forgot password?
|
|
88
|
-
</button>
|
|
89
|
-
{showHelp && (
|
|
90
|
-
<p className="text-[10px] text-[var(--text-secondary)] mt-1 bg-[var(--bg-tertiary)] rounded p-2">
|
|
91
|
-
Run in terminal:<br />
|
|
92
|
-
<code className="text-[var(--accent)]">forge --reset-password</code>
|
|
93
|
-
</p>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
</form>
|
|
97
|
-
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
9
|
+
return <LoginForm isRemote={isRemote} />;
|
|
101
10
|
}
|
package/app/page.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { auth } from '@/lib/auth';
|
|
2
2
|
import { redirect } from 'next/navigation';
|
|
3
3
|
import { headers } from 'next/headers';
|
|
4
|
-
import
|
|
4
|
+
import DashboardWrapper from '@/components/DashboardWrapper';
|
|
5
5
|
|
|
6
6
|
export default async function Home({ searchParams }: { searchParams: Promise<{ force?: string }> }) {
|
|
7
7
|
const session = await auth();
|
|
@@ -17,5 +17,5 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ f
|
|
|
17
17
|
if (isMobile) redirect('/mobile');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
return <
|
|
20
|
+
return <DashboardWrapper user={session.user} />;
|
|
21
21
|
}
|
package/bin/forge-server.mjs
CHANGED
|
@@ -64,6 +64,7 @@ const resetPassword = process.argv.includes('--reset-password');
|
|
|
64
64
|
|
|
65
65
|
const webPort = parseInt(getArg('--port')) || 8403;
|
|
66
66
|
const terminalPort = parseInt(getArg('--terminal-port')) || (webPort + 1);
|
|
67
|
+
const workspacePort = parseInt(getArg('--workspace-port')) || (webPort + 2);
|
|
67
68
|
const DATA_DIR = getArg('--dir')?.replace(/^~/, homedir()) || join(homedir(), '.forge', 'data');
|
|
68
69
|
|
|
69
70
|
const PID_FILE = join(DATA_DIR, 'forge.pid');
|
|
@@ -124,6 +125,7 @@ if (existsSync(envFile)) {
|
|
|
124
125
|
// Set env vars for Next.js and terminal server
|
|
125
126
|
process.env.PORT = String(webPort);
|
|
126
127
|
process.env.TERMINAL_PORT = String(terminalPort);
|
|
128
|
+
process.env.WORKSPACE_PORT = String(workspacePort);
|
|
127
129
|
process.env.FORGE_DATA_DIR = DATA_DIR;
|
|
128
130
|
|
|
129
131
|
// ── Password setup (first run or --reset-password) ──
|
|
@@ -255,7 +257,7 @@ function cleanupOrphans() {
|
|
|
255
257
|
}
|
|
256
258
|
// Kill standalone processes: our instance's + orphans without any tag
|
|
257
259
|
try {
|
|
258
|
-
const out = execSync(`ps aux | grep -E 'telegram-standalone|terminal-standalone' | grep -v grep`, {
|
|
260
|
+
const out = execSync(`ps aux | grep -E 'telegram-standalone|terminal-standalone|workspace-standalone' | grep -v grep`, {
|
|
259
261
|
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
260
262
|
}).trim();
|
|
261
263
|
for (const line of out.split('\n').filter(Boolean)) {
|
|
@@ -298,6 +300,16 @@ function startServices() {
|
|
|
298
300
|
services.push(telegramChild);
|
|
299
301
|
console.log(`[forge] Telegram bot started (pid: ${telegramChild.pid})`);
|
|
300
302
|
|
|
303
|
+
// Workspace daemon
|
|
304
|
+
const workspaceScript = join(ROOT, 'lib', 'workspace-standalone.ts');
|
|
305
|
+
const workspaceChild = spawn('npx', ['tsx', workspaceScript, instanceTag], {
|
|
306
|
+
cwd: ROOT,
|
|
307
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
308
|
+
env: { ...process.env },
|
|
309
|
+
});
|
|
310
|
+
services.push(workspaceChild);
|
|
311
|
+
console.log(`[forge] Workspace daemon started (pid: ${workspaceChild.pid})`);
|
|
312
|
+
|
|
301
313
|
// Track all child PIDs for clean shutdown
|
|
302
314
|
const childPids = services.map(c => c.pid).filter(Boolean);
|
|
303
315
|
savePids(childPids);
|
package/check-forge-status.sh
CHANGED
|
@@ -32,6 +32,15 @@ else
|
|
|
32
32
|
echo " ○ Telegram stopped"
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
# Workspace Daemon
|
|
36
|
+
count=$(ps aux | grep 'workspace-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
|
|
37
|
+
pid=$(ps aux | grep 'workspace-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
|
|
38
|
+
if [ "$count" -gt 0 ]; then
|
|
39
|
+
echo " ● Workspace running (pid: $pid)"
|
|
40
|
+
else
|
|
41
|
+
echo " ○ Workspace stopped"
|
|
42
|
+
fi
|
|
43
|
+
|
|
35
44
|
# Cloudflare Tunnel
|
|
36
45
|
count=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | wc -l | tr -d ' ')
|
|
37
46
|
pid=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | awk '{print $2}' | head -1)
|