@aion0/forge 0.1.0

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 (80) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +264 -0
  3. package/app/api/auth/[...nextauth]/route.ts +3 -0
  4. package/app/api/claude/[id]/route.ts +31 -0
  5. package/app/api/claude/[id]/stream/route.ts +63 -0
  6. package/app/api/claude/route.ts +28 -0
  7. package/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  8. package/app/api/claude-sessions/[projectName]/route.ts +37 -0
  9. package/app/api/claude-sessions/sync/route.ts +17 -0
  10. package/app/api/flows/route.ts +6 -0
  11. package/app/api/flows/run/route.ts +19 -0
  12. package/app/api/notify/test/route.ts +33 -0
  13. package/app/api/projects/route.ts +7 -0
  14. package/app/api/sessions/[id]/chat/route.ts +64 -0
  15. package/app/api/sessions/[id]/messages/route.ts +9 -0
  16. package/app/api/sessions/[id]/route.ts +17 -0
  17. package/app/api/sessions/route.ts +20 -0
  18. package/app/api/settings/route.ts +15 -0
  19. package/app/api/status/route.ts +12 -0
  20. package/app/api/tasks/[id]/route.ts +36 -0
  21. package/app/api/tasks/[id]/stream/route.ts +77 -0
  22. package/app/api/tasks/link/route.ts +37 -0
  23. package/app/api/tasks/route.ts +43 -0
  24. package/app/api/tasks/session/route.ts +14 -0
  25. package/app/api/templates/route.ts +6 -0
  26. package/app/api/tunnel/route.ts +20 -0
  27. package/app/api/watchers/route.ts +33 -0
  28. package/app/globals.css +26 -0
  29. package/app/icon.svg +26 -0
  30. package/app/layout.tsx +17 -0
  31. package/app/login/page.tsx +61 -0
  32. package/app/page.tsx +9 -0
  33. package/cli/mw.ts +377 -0
  34. package/components/ChatPanel.tsx +191 -0
  35. package/components/ClaudeTerminal.tsx +267 -0
  36. package/components/Dashboard.tsx +270 -0
  37. package/components/MarkdownContent.tsx +57 -0
  38. package/components/NewSessionModal.tsx +93 -0
  39. package/components/NewTaskModal.tsx +456 -0
  40. package/components/ProjectList.tsx +108 -0
  41. package/components/SessionList.tsx +74 -0
  42. package/components/SessionView.tsx +655 -0
  43. package/components/SettingsModal.tsx +366 -0
  44. package/components/StatusBar.tsx +99 -0
  45. package/components/TaskBoard.tsx +110 -0
  46. package/components/TaskDetail.tsx +351 -0
  47. package/components/TunnelToggle.tsx +163 -0
  48. package/components/WebTerminal.tsx +1069 -0
  49. package/docs/LOCAL-DEPLOY.md +144 -0
  50. package/docs/roadmap-multi-agent-workflow.md +330 -0
  51. package/instrumentation.ts +14 -0
  52. package/lib/auth.ts +47 -0
  53. package/lib/claude-process.ts +352 -0
  54. package/lib/claude-sessions.ts +267 -0
  55. package/lib/cloudflared.ts +218 -0
  56. package/lib/flows.ts +86 -0
  57. package/lib/init.ts +82 -0
  58. package/lib/notify.ts +75 -0
  59. package/lib/password.ts +77 -0
  60. package/lib/projects.ts +86 -0
  61. package/lib/session-manager.ts +156 -0
  62. package/lib/session-watcher.ts +345 -0
  63. package/lib/settings.ts +44 -0
  64. package/lib/task-manager.ts +668 -0
  65. package/lib/telegram-bot.ts +912 -0
  66. package/lib/terminal-server.ts +70 -0
  67. package/lib/terminal-standalone.ts +363 -0
  68. package/middleware.ts +33 -0
  69. package/next-env.d.ts +6 -0
  70. package/next.config.ts +16 -0
  71. package/package.json +66 -0
  72. package/postcss.config.mjs +7 -0
  73. package/src/config/index.ts +119 -0
  74. package/src/core/db/database.ts +133 -0
  75. package/src/core/memory/strategy.ts +32 -0
  76. package/src/core/providers/chat.ts +65 -0
  77. package/src/core/providers/registry.ts +60 -0
  78. package/src/core/session/manager.ts +190 -0
  79. package/src/types/index.ts +128 -0
  80. package/tsconfig.json +41 -0
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getSessionManager } from '@/lib/session-manager';
3
+
4
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
+ const { id } = await params;
6
+ const manager = getSessionManager();
7
+ const session = manager.get(id) || manager.getByName(id);
8
+ if (!session) return NextResponse.json({ error: 'Not found' }, { status: 404 });
9
+ return NextResponse.json(session);
10
+ }
11
+
12
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
13
+ const { id } = await params;
14
+ const manager = getSessionManager();
15
+ manager.delete(id);
16
+ return NextResponse.json({ ok: true });
17
+ }
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getSessionManager } from '@/lib/session-manager';
3
+ import { loadAllTemplates } from '@/src/config';
4
+
5
+ export async function GET() {
6
+ const manager = getSessionManager();
7
+ const sessions = manager.list();
8
+ return NextResponse.json(sessions);
9
+ }
10
+
11
+ export async function POST(req: Request) {
12
+ const body = await req.json();
13
+ const manager = getSessionManager();
14
+ try {
15
+ const session = manager.create(body);
16
+ return NextResponse.json(session);
17
+ } catch (err: any) {
18
+ return NextResponse.json({ error: err.message }, { status: 400 });
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { loadSettings, saveSettings, type Settings } from '@/lib/settings';
3
+ import { restartTelegramBot } from '@/lib/init';
4
+
5
+ export async function GET() {
6
+ return NextResponse.json(loadSettings());
7
+ }
8
+
9
+ export async function PUT(req: Request) {
10
+ const body = await req.json() as Settings;
11
+ saveSettings(body);
12
+ // Restart Telegram bot in case token/chatId changed
13
+ restartTelegramBot();
14
+ return NextResponse.json({ ok: true });
15
+ }
@@ -0,0 +1,12 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getSessionManager } from '@/lib/session-manager';
3
+ import { listAvailableProviders } from '@/src/core/providers/registry';
4
+
5
+ export async function GET() {
6
+ const manager = getSessionManager();
7
+ return NextResponse.json({
8
+ sessions: manager.list(),
9
+ providers: listAvailableProviders(),
10
+ usage: manager.getUsageSummary(),
11
+ });
12
+ }
@@ -0,0 +1,36 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getTask, cancelTask, deleteTask, retryTask } from '@/lib/task-manager';
3
+
4
+ // Get task details (including full log)
5
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params;
7
+ const task = getTask(id);
8
+ if (!task) return NextResponse.json({ error: 'Not found' }, { status: 404 });
9
+ return NextResponse.json(task);
10
+ }
11
+
12
+ // Actions: cancel, retry
13
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
14
+ const { id } = await params;
15
+ const { action } = await req.json();
16
+
17
+ if (action === 'cancel') {
18
+ const ok = cancelTask(id);
19
+ return NextResponse.json({ ok });
20
+ }
21
+
22
+ if (action === 'retry') {
23
+ const newTask = retryTask(id);
24
+ if (!newTask) return NextResponse.json({ error: 'Cannot retry this task' }, { status: 400 });
25
+ return NextResponse.json(newTask);
26
+ }
27
+
28
+ return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
29
+ }
30
+
31
+ // Delete a task
32
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
33
+ const { id } = await params;
34
+ deleteTask(id);
35
+ return NextResponse.json({ ok: true });
36
+ }
@@ -0,0 +1,77 @@
1
+ import { getTask } from '@/lib/task-manager';
2
+ import { onTaskEvent } from '@/lib/task-manager';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+ export const runtime = 'nodejs';
6
+
7
+ // SSE stream for real-time task log updates
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params;
10
+
11
+ const task = getTask(id);
12
+ if (!task) {
13
+ return new Response('Task not found', { status: 404 });
14
+ }
15
+
16
+ const encoder = new TextEncoder();
17
+ let unsubscribe: (() => void) | null = null;
18
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
19
+ let closed = false;
20
+
21
+ const stream = new ReadableStream({
22
+ start(controller) {
23
+ // Send existing log entries
24
+ for (const entry of task.log) {
25
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'log', entry })}\n\n`));
26
+ }
27
+
28
+ // Send current status
29
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'status', status: task.status })}\n\n`));
30
+
31
+ // Heartbeat
32
+ heartbeat = setInterval(() => {
33
+ if (!closed) {
34
+ try { controller.enqueue(encoder.encode(': heartbeat\n\n')); } catch { cleanup(); }
35
+ }
36
+ }, 15000);
37
+
38
+ // Listen for new events
39
+ unsubscribe = onTaskEvent((taskId, event, data) => {
40
+ if (taskId !== id || closed) return;
41
+ try {
42
+ if (event === 'log') {
43
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'log', entry: data })}\n\n`));
44
+ } else if (event === 'status') {
45
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'status', status: data })}\n\n`));
46
+ // Close stream when task is done
47
+ if (data === 'done' || data === 'failed' || data === 'cancelled') {
48
+ // Send final task data
49
+ const finalTask = getTask(id);
50
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'complete', task: finalTask })}\n\n`));
51
+ cleanup();
52
+ controller.close();
53
+ }
54
+ }
55
+ } catch { cleanup(); }
56
+ });
57
+ },
58
+ cancel() {
59
+ cleanup();
60
+ },
61
+ });
62
+
63
+ function cleanup() {
64
+ closed = true;
65
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
66
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
67
+ }
68
+
69
+ return new Response(stream, {
70
+ headers: {
71
+ 'Content-Type': 'text/event-stream',
72
+ 'Cache-Control': 'no-cache, no-transform',
73
+ Connection: 'keep-alive',
74
+ 'X-Accel-Buffering': 'no',
75
+ },
76
+ });
77
+ }
@@ -0,0 +1,37 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { createTask } from '@/lib/task-manager';
3
+ import { getProjectInfo } from '@/lib/projects';
4
+ import { getDb } from '@/src/core/db/database';
5
+ import { getDbPath } from '@/src/config';
6
+
7
+ /**
8
+ * Link an existing local Claude Code session to a project.
9
+ * Creates a placeholder task with the conversation_id so future tasks
10
+ * for this project automatically continue that session.
11
+ */
12
+ export async function POST(req: Request) {
13
+ const { projectName, conversationId } = await req.json();
14
+
15
+ if (!projectName || !conversationId) {
16
+ return NextResponse.json({ error: 'projectName and conversationId required' }, { status: 400 });
17
+ }
18
+
19
+ const project = getProjectInfo(projectName);
20
+ if (!project) {
21
+ return NextResponse.json({ error: `Project not found: ${projectName}` }, { status: 404 });
22
+ }
23
+
24
+ // Create a placeholder "done" task that carries the conversation_id
25
+ const db = getDb(getDbPath());
26
+ const id = `link-${Date.now().toString(36)}`;
27
+ db.prepare(`
28
+ INSERT INTO tasks (id, project_name, project_path, prompt, status, priority, conversation_id, log, result_summary, completed_at)
29
+ VALUES (?, ?, ?, ?, 'done', 0, ?, '[]', ?, datetime('now'))
30
+ `).run(id, project.name, project.path, '(linked from local CLI)', conversationId, `Session ${conversationId} linked from local CLI`);
31
+
32
+ return NextResponse.json({
33
+ id,
34
+ projectName: project.name,
35
+ conversationId,
36
+ });
37
+ }
@@ -0,0 +1,43 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { createTask, listTasks } from '@/lib/task-manager';
3
+ import { ensureInitialized } from '@/lib/init';
4
+ import { getProjectInfo } from '@/lib/projects';
5
+ import type { TaskStatus } from '@/src/types';
6
+
7
+ // List tasks — optionally filter by status
8
+ export async function GET(req: Request) {
9
+ ensureInitialized();
10
+ const url = new URL(req.url);
11
+ const status = url.searchParams.get('status') as TaskStatus | null;
12
+ return NextResponse.json(listTasks(status || undefined));
13
+ }
14
+
15
+ // Create a new task
16
+ export async function POST(req: Request) {
17
+ const { projectName, prompt, priority, newSession, conversationId, scheduledAt, mode, watchConfig } = await req.json();
18
+
19
+ if (!projectName || !prompt) {
20
+ return NextResponse.json({ error: 'projectName and prompt are required' }, { status: 400 });
21
+ }
22
+
23
+ const project = getProjectInfo(projectName);
24
+ if (!project) {
25
+ return NextResponse.json({ error: `Project not found: ${projectName}` }, { status: 404 });
26
+ }
27
+
28
+ // conversationId: explicit value → use it; newSession → empty string (force new); otherwise → auto-inherit
29
+ const convId = conversationId || (newSession ? '' : undefined);
30
+
31
+ const task = createTask({
32
+ projectName: project.name,
33
+ projectPath: project.path,
34
+ prompt,
35
+ priority: priority || 0,
36
+ conversationId: convId,
37
+ scheduledAt: scheduledAt || undefined,
38
+ mode: mode || 'prompt',
39
+ watchConfig: watchConfig || undefined,
40
+ });
41
+
42
+ return NextResponse.json(task);
43
+ }
@@ -0,0 +1,14 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getProjectConversationId } from '@/lib/task-manager';
3
+
4
+ export async function GET(req: Request) {
5
+ const url = new URL(req.url);
6
+ const project = url.searchParams.get('project');
7
+
8
+ if (!project) {
9
+ return NextResponse.json({ error: 'project parameter required' }, { status: 400 });
10
+ }
11
+
12
+ const conversationId = getProjectConversationId(project);
13
+ return NextResponse.json({ conversationId });
14
+ }
@@ -0,0 +1,6 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { loadAllTemplates } from '@/src/config';
3
+
4
+ export async function GET() {
5
+ return NextResponse.json(loadAllTemplates());
6
+ }
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { startTunnel, stopTunnel, getTunnelStatus } from '@/lib/cloudflared';
3
+
4
+ /** GET /api/tunnel — current tunnel status */
5
+ export async function GET() {
6
+ return NextResponse.json(getTunnelStatus());
7
+ }
8
+
9
+ /** POST /api/tunnel — start or stop tunnel */
10
+ export async function POST(req: Request) {
11
+ const { action } = await req.json() as { action: 'start' | 'stop' };
12
+
13
+ if (action === 'stop') {
14
+ stopTunnel();
15
+ return NextResponse.json({ ok: true, ...getTunnelStatus() });
16
+ }
17
+
18
+ const result = await startTunnel();
19
+ return NextResponse.json({ ok: !result.error, ...getTunnelStatus() });
20
+ }
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { ensureInitialized } from '@/lib/init';
3
+ import { listWatchers, createWatcher, deleteWatcher, toggleWatcher } from '@/lib/session-watcher';
4
+
5
+ export async function GET() {
6
+ ensureInitialized();
7
+ return NextResponse.json(listWatchers());
8
+ }
9
+
10
+ export async function POST(req: Request) {
11
+ ensureInitialized();
12
+ const body = await req.json();
13
+
14
+ if (body.action === 'delete') {
15
+ deleteWatcher(body.id);
16
+ return NextResponse.json({ ok: true });
17
+ }
18
+
19
+ if (body.action === 'toggle') {
20
+ toggleWatcher(body.id, body.active);
21
+ return NextResponse.json({ ok: true });
22
+ }
23
+
24
+ // Create new watcher
25
+ const watcher = createWatcher({
26
+ projectName: body.projectName,
27
+ sessionId: body.sessionId,
28
+ label: body.label,
29
+ checkInterval: body.checkInterval || 60,
30
+ });
31
+
32
+ return NextResponse.json(watcher);
33
+ }
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --bg-primary: #0a0a0a;
5
+ --bg-secondary: #141414;
6
+ --bg-tertiary: #1e1e1e;
7
+ --border: #2a2a2a;
8
+ --text-primary: #e5e5e5;
9
+ --text-secondary: #999;
10
+ --accent: #3b82f6;
11
+ --green: #22c55e;
12
+ --yellow: #eab308;
13
+ --red: #ef4444;
14
+ }
15
+
16
+ body {
17
+ background: var(--bg-primary);
18
+ color: var(--text-primary);
19
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
20
+ }
21
+
22
+ /* Scrollbar */
23
+ ::-webkit-scrollbar { width: 6px; }
24
+ ::-webkit-scrollbar-track { background: var(--bg-primary); }
25
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
26
+
package/app/icon.svg ADDED
@@ -0,0 +1,26 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
2
+ <defs>
3
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#60a5fa"/>
5
+ <stop offset="100%" stop-color="#3b82f6"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <!-- Background -->
9
+ <rect width="32" height="32" rx="7" fill="#1a1a2e"/>
10
+ <!-- Anvil base -->
11
+ <rect x="6" y="22" width="20" height="3" rx="1" fill="url(#g)"/>
12
+ <!-- Anvil middle -->
13
+ <rect x="9" y="18" width="14" height="4" fill="url(#g)" opacity="0.9"/>
14
+ <!-- Anvil top face -->
15
+ <polygon points="9,15 23,15 23,18 9,18" fill="url(#g)" opacity="0.85"/>
16
+ <!-- Anvil horn (left) -->
17
+ <polygon points="9,15 4,17 4,18 9,18" fill="#60a5fa" opacity="0.8"/>
18
+ <!-- Hammer handle -->
19
+ <rect x="19" y="4" width="2.5" height="10" rx="1" fill="#f59e0b" transform="rotate(-35 20.25 9)"/>
20
+ <!-- Hammer head -->
21
+ <rect x="17" y="2.5" width="7" height="3.5" rx="1" fill="#f59e0b" transform="rotate(-35 20.5 4.25)"/>
22
+ <!-- Sparks -->
23
+ <circle cx="14" cy="12.5" r="1.2" fill="#fbbf24" opacity="0.9"/>
24
+ <circle cx="11" cy="10.5" r="0.8" fill="#fbbf24" opacity="0.7"/>
25
+ <circle cx="17" cy="10" r="0.7" fill="#fbbf24" opacity="0.6"/>
26
+ </svg>
package/app/layout.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import type { Metadata } from 'next';
2
+ import './globals.css';
3
+
4
+ export const metadata: Metadata = {
5
+ title: 'Forge',
6
+ description: 'Unified AI workflow platform',
7
+ };
8
+
9
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <html lang="en" className="dark">
12
+ <body className="min-h-screen bg-[var(--bg-primary)]">
13
+ {children}
14
+ </body>
15
+ </html>
16
+ );
17
+ }
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { signIn } from 'next-auth/react';
5
+
6
+ export default function LoginPage() {
7
+ const [password, setPassword] = useState('');
8
+ const [error, setError] = useState('');
9
+
10
+ const handleLocal = async (e: React.FormEvent) => {
11
+ e.preventDefault();
12
+ const result = await signIn('credentials', {
13
+ password,
14
+ callbackUrl: window.location.origin + '/',
15
+ }) as { error?: string } | undefined;
16
+ if (result?.error) setError('Wrong password');
17
+ };
18
+
19
+ return (
20
+ <div className="min-h-screen flex items-center justify-center">
21
+ <div className="w-80 space-y-6">
22
+ <div className="text-center">
23
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">Forge</h1>
24
+ <p className="text-sm text-[var(--text-secondary)] mt-1">Unified AI Platform</p>
25
+ </div>
26
+
27
+ {/* Local password login */}
28
+ <form onSubmit={handleLocal} className="space-y-3">
29
+ <input
30
+ type="password"
31
+ value={password}
32
+ onChange={e => setPassword(e.target.value)}
33
+ placeholder="Password"
34
+ 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)]"
35
+ />
36
+ {error && <p className="text-xs text-[var(--red)]">{error}</p>}
37
+ <button
38
+ type="submit"
39
+ className="w-full py-2 bg-[var(--accent)] text-white rounded text-sm hover:opacity-90"
40
+ >
41
+ Sign In
42
+ </button>
43
+ </form>
44
+
45
+ <div className="flex items-center gap-3 text-[var(--text-secondary)] text-xs">
46
+ <div className="flex-1 h-px bg-[var(--border)]" />
47
+ <span>or</span>
48
+ <div className="flex-1 h-px bg-[var(--border)]" />
49
+ </div>
50
+
51
+ {/* Google OAuth */}
52
+ <button
53
+ onClick={() => signIn('google', { callbackUrl: window.location.origin + '/' })}
54
+ className="w-full py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] hover:bg-[var(--border)] transition-colors"
55
+ >
56
+ Sign in with Google
57
+ </button>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,9 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import Dashboard from '@/components/Dashboard';
4
+
5
+ export default async function Home() {
6
+ const session = await auth();
7
+ if (!session) redirect('/login');
8
+ return <Dashboard user={session.user} />;
9
+ }