@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.
Files changed (93) hide show
  1. package/README.md +27 -2
  2. package/RELEASE_NOTES.md +21 -14
  3. package/app/api/agents/route.ts +17 -0
  4. package/app/api/delivery/[id]/route.ts +62 -0
  5. package/app/api/delivery/route.ts +40 -0
  6. package/app/api/mobile-chat/route.ts +13 -7
  7. package/app/api/monitor/route.ts +10 -6
  8. package/app/api/pipelines/[id]/route.ts +16 -3
  9. package/app/api/tasks/route.ts +2 -1
  10. package/app/api/workspace/[id]/agents/route.ts +35 -0
  11. package/app/api/workspace/[id]/memory/route.ts +23 -0
  12. package/app/api/workspace/[id]/smith/route.ts +22 -0
  13. package/app/api/workspace/[id]/stream/route.ts +28 -0
  14. package/app/api/workspace/route.ts +100 -0
  15. package/app/global-error.tsx +10 -4
  16. package/app/icon.ico +0 -0
  17. package/app/layout.tsx +2 -2
  18. package/app/login/LoginForm.tsx +96 -0
  19. package/app/login/page.tsx +7 -98
  20. package/app/page.tsx +2 -2
  21. package/bin/forge-server.mjs +13 -1
  22. package/check-forge-status.sh +9 -0
  23. package/components/ConversationEditor.tsx +411 -0
  24. package/components/ConversationGraphView.tsx +347 -0
  25. package/components/ConversationTerminalView.tsx +303 -0
  26. package/components/Dashboard.tsx +36 -39
  27. package/components/DashboardWrapper.tsx +9 -0
  28. package/components/DeliveryFlowEditor.tsx +491 -0
  29. package/components/DeliveryList.tsx +230 -0
  30. package/components/DeliveryWorkspace.tsx +589 -0
  31. package/components/DocTerminal.tsx +10 -2
  32. package/components/DocsViewer.tsx +10 -2
  33. package/components/HelpTerminal.tsx +11 -6
  34. package/components/InlinePipelineView.tsx +111 -0
  35. package/components/MobileView.tsx +20 -0
  36. package/components/MonitorPanel.tsx +9 -4
  37. package/components/NewTaskModal.tsx +32 -0
  38. package/components/PipelineEditor.tsx +49 -6
  39. package/components/PipelineView.tsx +482 -64
  40. package/components/ProjectDetail.tsx +314 -56
  41. package/components/ProjectManager.tsx +49 -4
  42. package/components/SessionView.tsx +27 -13
  43. package/components/SettingsModal.tsx +790 -124
  44. package/components/SkillsPanel.tsx +31 -8
  45. package/components/TaskBoard.tsx +3 -0
  46. package/components/WebTerminal.tsx +257 -43
  47. package/components/WorkspaceTree.tsx +221 -0
  48. package/components/WorkspaceView.tsx +2245 -0
  49. package/install.sh +2 -2
  50. package/lib/agents/claude-adapter.ts +104 -0
  51. package/lib/agents/generic-adapter.ts +64 -0
  52. package/lib/agents/index.ts +242 -0
  53. package/lib/agents/types.ts +70 -0
  54. package/lib/artifacts.ts +106 -0
  55. package/lib/delivery.ts +787 -0
  56. package/lib/forge-skills/forge-inbox.md +37 -0
  57. package/lib/forge-skills/forge-send.md +40 -0
  58. package/lib/forge-skills/forge-status.md +32 -0
  59. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  60. package/lib/help-docs/00-overview.md +7 -1
  61. package/lib/help-docs/01-settings.md +159 -2
  62. package/lib/help-docs/05-pipelines.md +89 -0
  63. package/lib/help-docs/07-projects.md +35 -1
  64. package/lib/help-docs/11-workspace.md +254 -0
  65. package/lib/help-docs/CLAUDE.md +7 -2
  66. package/lib/init.ts +60 -10
  67. package/lib/pipeline.ts +537 -1
  68. package/lib/settings.ts +115 -22
  69. package/lib/skills.ts +249 -372
  70. package/lib/task-manager.ts +113 -33
  71. package/lib/telegram-bot.ts +33 -1
  72. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  73. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  74. package/lib/workspace/agent-bus.ts +416 -0
  75. package/lib/workspace/agent-worker.ts +667 -0
  76. package/lib/workspace/backends/api-backend.ts +262 -0
  77. package/lib/workspace/backends/cli-backend.ts +479 -0
  78. package/lib/workspace/index.ts +82 -0
  79. package/lib/workspace/manager.ts +136 -0
  80. package/lib/workspace/orchestrator.ts +1914 -0
  81. package/lib/workspace/persistence.ts +310 -0
  82. package/lib/workspace/presets.ts +170 -0
  83. package/lib/workspace/skill-installer.ts +188 -0
  84. package/lib/workspace/smith-memory.ts +498 -0
  85. package/lib/workspace/types.ts +231 -0
  86. package/lib/workspace/watch-manager.ts +288 -0
  87. package/lib/workspace-standalone.ts +814 -0
  88. package/middleware.ts +1 -0
  89. package/next-env.d.ts +1 -1
  90. package/package.json +4 -1
  91. package/src/config/index.ts +12 -1
  92. package/src/core/db/database.ts +1 -0
  93. 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
+ }
@@ -1,101 +1,10 @@
1
- 'use client';
1
+ import { headers } from 'next/headers';
2
+ import LoginForm from './LoginForm';
2
3
 
3
- import { useState, useEffect } from 'react';
4
- import { signIn } from 'next-auth/react';
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
- export default function LoginPage() {
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 Dashboard from '@/components/Dashboard';
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 <Dashboard user={session.user} />;
20
+ return <DashboardWrapper user={session.user} />;
21
21
  }
@@ -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);
@@ -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)