@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
package/CLAUDE.md ADDED
@@ -0,0 +1,4 @@
1
+ ## Obsidian Vault
2
+ Location: /Users/zliu/MyDocuments/obsidian-project/Projects/Bastion
3
+ When I ask about my notes, use bash to search and read files from this directory.
4
+ Example: find /Users/zliu/MyDocuments/obsidian-project -name "*.md" | head -20
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # Forge
2
+
3
+ > Self-hosted AI workflow platform — web terminal, task orchestration, remote access.
4
+
5
+ Forge is a self-hosted web platform built around [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It provides a browser-based terminal backed by tmux, a task queue for running Claude Code in the background, and one-click remote access via Cloudflare Tunnel — all behind a simple daily-rotating password.
6
+
7
+ No API keys required. Forge runs on your existing Claude Code subscription.
8
+
9
+ ## Features
10
+
11
+ - **Web Terminal** — Full tmux-backed terminal in the browser. Multiple tabs, persistent sessions that survive page refresh, browser close, and server restart
12
+ - **Task Orchestration** — Submit tasks to Claude Code, queue them by project, track progress with live streaming output
13
+ - **Remote Access** — One-click Cloudflare Tunnel for a secure public URL (zero config, no account needed)
14
+ - **Session Continuity** — Tasks for the same project automatically continue the previous conversation context
15
+ - **YAML Workflows** — Define multi-step flows that chain tasks together
16
+ - **Bot Integration** — Telegram bot for mobile task management and tunnel control (extensible to other platforms)
17
+ - **Session Watcher** — Monitor Claude Code sessions for changes, idle state, keywords, or errors
18
+ - **CLI** — Full-featured command-line interface for task management
19
+ - **Auth** — Auto-generated daily rotating password + optional Google OAuth
20
+
21
+ ## Prerequisites
22
+
23
+ - **Node.js** >= 20
24
+ - **pnpm** (recommended) or npm
25
+ - **tmux** — for web terminal sessions
26
+ - **Claude Code CLI** — `npm install -g @anthropic-ai/claude-code`
27
+
28
+ ## Installation
29
+
30
+ ### From npm
31
+
32
+ ```bash
33
+ npm install -g @aion0/forge
34
+ ```
35
+
36
+ ### From source
37
+
38
+ ```bash
39
+ git clone https://github.com/aiwatching/forge.git
40
+ cd forge
41
+ pnpm install
42
+ pnpm build
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ### 1. Create config
48
+
49
+ Create `.env.local` in the project root:
50
+
51
+ ```env
52
+ # Auth (generate a random string, e.g. openssl rand -hex 32)
53
+ AUTH_SECRET=<random-string>
54
+ AUTH_TRUST_HOST=true
55
+
56
+ # Optional: Google OAuth for production
57
+ # GOOGLE_CLIENT_ID=...
58
+ # GOOGLE_CLIENT_SECRET=...
59
+ ```
60
+
61
+ > **API keys are not required.** Forge uses your local Claude Code CLI, which runs on your Anthropic subscription. If you want to use the built-in multi-model chat feature, you can optionally add provider keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `XAI_API_KEY`) later.
62
+
63
+ ### 2. Start the server
64
+
65
+ ```bash
66
+ # Development
67
+ pnpm dev
68
+
69
+ # Production
70
+ pnpm build && pnpm start
71
+ ```
72
+
73
+ ### 3. Log in
74
+
75
+ Open `http://localhost:3000`. A login password is auto-generated and printed in the console:
76
+
77
+ ```
78
+ [init] Login password: a7x9k2 (valid today)
79
+ ```
80
+
81
+ The password rotates daily. Forgot it? Run:
82
+
83
+ ```bash
84
+ forge password
85
+ ```
86
+
87
+ ### 4. Configure projects
88
+
89
+ Open **Settings** (gear icon) and add your project root directories (e.g. `~/Projects`). Forge will scan for git repositories automatically.
90
+
91
+ ## Web Terminal
92
+
93
+ The core feature. A browser-based terminal powered by tmux:
94
+
95
+ - **Persistent** — Sessions survive page refresh, browser close, and server restart
96
+ - **Multi-tab** — Create, rename, and manage multiple terminal tabs
97
+ - **Remote-ready** — Access your terminal from anywhere via Cloudflare Tunnel
98
+ - **Large scrollback** — 50,000 lines with mouse support
99
+
100
+ The terminal server runs on `localhost:3001` and is auto-proxied through the main app for remote access.
101
+
102
+ ## Remote Access (Cloudflare Tunnel)
103
+
104
+ Access Forge from anywhere without port forwarding or DNS config:
105
+
106
+ 1. Click the **tunnel icon** in the header bar, or go to **Settings > Remote Access**
107
+ 2. Click **Start** — Forge auto-downloads `cloudflared` and creates a temporary public URL
108
+ 3. The URL is protected by the daily login password
109
+
110
+ Enable **Auto-start** in Settings to start the tunnel on every server boot.
111
+
112
+ > The tunnel URL changes each time. Use the Telegram bot `/tunnel_password` command to get the current URL and password on your phone.
113
+
114
+ ## Task Orchestration
115
+
116
+ Submit AI coding tasks that run in the background:
117
+
118
+ ```bash
119
+ # Submit a task
120
+ forge task my-app "Fix the login bug in auth.ts"
121
+
122
+ # Force a fresh session (ignore previous context)
123
+ forge task my-app "Refactor the API layer" --new
124
+
125
+ # List tasks
126
+ forge tasks # all
127
+ forge tasks running # filter by status
128
+
129
+ # Watch task output live
130
+ forge watch <task-id>
131
+
132
+ # Task details (result, git diff, cost)
133
+ forge status <task-id>
134
+
135
+ # Cancel / retry
136
+ forge cancel <task-id>
137
+ forge retry <task-id>
138
+ ```
139
+
140
+ **All CLI shortcuts:** `t`=task, `r`=run, `ls`=tasks, `w`=watch, `l`=log, `s`=status, `f`=flows, `p`=projects, `pw`=password
141
+
142
+ ## YAML Workflows
143
+
144
+ Define multi-step flows in `~/.my-workflow/flows/`:
145
+
146
+ ```yaml
147
+ # ~/.my-workflow/flows/daily-review.yaml
148
+ name: daily-review
149
+ steps:
150
+ - project: my-app
151
+ prompt: "Review open TODOs and suggest fixes"
152
+ - project: my-api
153
+ prompt: "Check for any failing tests and fix them"
154
+ ```
155
+
156
+ Run with `forge run daily-review`.
157
+
158
+ ## Bot Integration
159
+
160
+ Forge ships with a Telegram bot for mobile-friendly control. The bot system is designed to be extensible to other platforms in the future.
161
+
162
+ ### Telegram Setup
163
+
164
+ 1. Create a bot via [@BotFather](https://t.me/botfather)
165
+ 2. In **Settings**, add your **Bot Token** and **Chat ID**
166
+ 3. Optionally set a **Tunnel Password** for remote access control
167
+
168
+ ### Commands
169
+
170
+ | Command | Description |
171
+ |---------|-------------|
172
+ | `/tasks` | List tasks with quick-action numbers |
173
+ | `/tasks running` | Filter by status |
174
+ | `/sessions` | Browse Claude Code sessions |
175
+ | `/watch <project>` | Monitor a session for changes |
176
+ | `/tunnel start <pw>` | Start Cloudflare Tunnel |
177
+ | `/tunnel stop <pw>` | Stop tunnel |
178
+ | `/tunnel_password <pw>` | Get login password + tunnel URL |
179
+ | `/help` | Show all commands |
180
+
181
+ Password-protected commands auto-delete your message to keep credentials safe.
182
+
183
+ ## Configuration
184
+
185
+ All config lives in `~/.my-workflow/`:
186
+
187
+ ```
188
+ ~/.my-workflow/
189
+ settings.yaml # Main configuration
190
+ password.json # Daily auto-generated login password
191
+ data.db # SQLite database (tasks, sessions)
192
+ terminal-state.json # Terminal tab layout
193
+ flows/ # YAML workflow definitions
194
+ bin/ # Auto-downloaded binaries (cloudflared)
195
+ ```
196
+
197
+ ### settings.yaml
198
+
199
+ ```yaml
200
+ # Project directories to scan
201
+ projectRoots:
202
+ - ~/Projects
203
+ - ~/Work
204
+
205
+ # Claude Code binary path (default: claude)
206
+ claudePath: claude
207
+
208
+ # Cloudflare Tunnel
209
+ tunnelAutoStart: false # Auto-start on server boot
210
+
211
+ # Telegram bot (optional)
212
+ telegramBotToken: "" # Bot API token from @BotFather
213
+ telegramChatId: "" # Your chat ID
214
+ telegramTunnelPassword: "" # Password for tunnel commands
215
+
216
+ # Task notifications (optional, requires Telegram)
217
+ notifyOnComplete: true
218
+ notifyOnFailure: true
219
+ ```
220
+
221
+ ## Architecture
222
+
223
+ ```
224
+ ┌─────────────────────────────────────────────┐
225
+ │ Web Dashboard (Next.js + React) │
226
+ │ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
227
+ │ │ Tasks │ │ Sessions │ │ Terminal │ │
228
+ │ └──────────┘ └──────────┘ └─────────────┘ │
229
+ ├─────────────────────────────────────────────┤
230
+ │ API Layer (Next.js Route Handlers) │
231
+ ├──────────┬──────────┬───────────────────────┤
232
+ │ Claude │ Task │ Bot Integration │
233
+ │ Code │ Runner │ (Telegram, ...) │
234
+ │ Process │ (Queue) │ │
235
+ ├──────────┴──────────┴───────────────────────┤
236
+ │ SQLite (better-sqlite3) │
237
+ ├─────────────────────────────────────────────┤
238
+ │ Terminal Server (node-pty + tmux + WS) │
239
+ ├─────────────────────────────────────────────┤
240
+ │ Cloudflare Tunnel (optional) │
241
+ └─────────────────────────────────────────────┘
242
+ ```
243
+
244
+ ## Tech Stack
245
+
246
+ | Layer | Technology |
247
+ |-------|-----------|
248
+ | Frontend | Next.js 16, React 19, Tailwind CSS 4 |
249
+ | Backend | Next.js Route Handlers, SQLite |
250
+ | Terminal | xterm.js, node-pty, tmux, WebSocket |
251
+ | Auth | NextAuth v5 |
252
+ | Tunnel | Cloudflare (cloudflared) |
253
+ | Bot | Telegram Bot API (extensible) |
254
+
255
+ ## Roadmap
256
+
257
+ - [ ] **Multi-Agent Workflow** — DAG-based pipelines where multiple Claude Code instances collaborate, passing outputs between nodes with conditional routing and parallel execution. See [docs/roadmap-multi-agent-workflow.md](docs/roadmap-multi-agent-workflow.md).
258
+ - [ ] Pipeline UI — DAG visualization with real-time node status
259
+ - [ ] Additional bot platforms — Discord, Slack, etc.
260
+ - [ ] Multi-model chat with API keys (Anthropic, OpenAI, Google, xAI)
261
+
262
+ ## License
263
+
264
+ MIT
@@ -0,0 +1,3 @@
1
+ import { handlers } from '@/lib/auth';
2
+
3
+ export const { GET, POST } = handlers;
@@ -0,0 +1,31 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getProcess, sendToClaudeSession, killProcess } from '@/lib/claude-process';
3
+
4
+ // Get session info
5
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params;
7
+ const proc = getProcess(id);
8
+ if (!proc) return NextResponse.json({ error: 'Not found' }, { status: 404 });
9
+ return NextResponse.json(proc);
10
+ }
11
+
12
+ // Send a message to the Claude session
13
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
14
+ const { id } = await params;
15
+ const body = await req.json();
16
+
17
+ if (body.type === 'message') {
18
+ const ok = sendToClaudeSession(id, body.content, body.conversationId);
19
+ if (!ok) {
20
+ return NextResponse.json({ error: 'Session not found or already running' }, { status: 400 });
21
+ }
22
+ return NextResponse.json({ ok: true });
23
+ }
24
+
25
+ if (body.type === 'kill') {
26
+ killProcess(id);
27
+ return NextResponse.json({ ok: true });
28
+ }
29
+
30
+ return NextResponse.json({ error: 'Unknown type' }, { status: 400 });
31
+ }
@@ -0,0 +1,63 @@
1
+ import { attachToProcess } from '@/lib/claude-process';
2
+
3
+ export const dynamic = 'force-dynamic';
4
+ export const runtime = 'nodejs';
5
+
6
+ // SSE stream of Claude Code structured messages
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params;
9
+
10
+ const encoder = new TextEncoder();
11
+ let unsubscribe: (() => void) | null = null;
12
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
13
+ let closed = false;
14
+
15
+ const stream = new ReadableStream({
16
+ start(controller) {
17
+ // Heartbeat every 15s to keep connection alive
18
+ heartbeat = setInterval(() => {
19
+ if (!closed) {
20
+ try {
21
+ controller.enqueue(encoder.encode(': heartbeat\n\n'));
22
+ } catch {
23
+ // Controller closed
24
+ cleanup();
25
+ }
26
+ }
27
+ }, 15000);
28
+
29
+ unsubscribe = attachToProcess(id, (msg) => {
30
+ if (closed) return;
31
+ try {
32
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(msg)}\n\n`));
33
+ } catch {
34
+ cleanup();
35
+ }
36
+ });
37
+
38
+ if (!unsubscribe) {
39
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'system', subtype: 'error', content: 'Session not found' })}\n\n`));
40
+ cleanup();
41
+ controller.close();
42
+ }
43
+ },
44
+ cancel() {
45
+ cleanup();
46
+ },
47
+ });
48
+
49
+ function cleanup() {
50
+ closed = true;
51
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
52
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
53
+ }
54
+
55
+ return new Response(stream, {
56
+ headers: {
57
+ 'Content-Type': 'text/event-stream',
58
+ 'Cache-Control': 'no-cache, no-transform',
59
+ Connection: 'keep-alive',
60
+ 'X-Accel-Buffering': 'no',
61
+ },
62
+ });
63
+ }
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { createClaudeSession, listProcesses, deleteSession } from '@/lib/claude-process';
3
+ import { getProjectInfo } from '@/lib/projects';
4
+
5
+ // List all Claude Code sessions
6
+ export async function GET() {
7
+ return NextResponse.json(listProcesses());
8
+ }
9
+
10
+ // Create a new Claude Code session for a project
11
+ export async function POST(req: Request) {
12
+ const { projectName } = await req.json();
13
+
14
+ const project = getProjectInfo(projectName);
15
+ if (!project) {
16
+ return NextResponse.json({ error: `Project not found: ${projectName}` }, { status: 404 });
17
+ }
18
+
19
+ const session = createClaudeSession(project.name, project.path);
20
+ return NextResponse.json(session);
21
+ }
22
+
23
+ // Delete a session
24
+ export async function DELETE(req: Request) {
25
+ const { id } = await req.json();
26
+ deleteSession(id);
27
+ return NextResponse.json({ ok: true });
28
+ }
@@ -0,0 +1,72 @@
1
+ import { getSessionFilePath, readSessionEntries, tailSessionFile } from '@/lib/claude-sessions';
2
+
3
+ export const dynamic = 'force-dynamic';
4
+ export const runtime = 'nodejs';
5
+
6
+ export async function GET(req: Request, { params }: { params: Promise<{ projectName: string }> }) {
7
+ const { projectName } = await params;
8
+ const url = new URL(req.url);
9
+ const sessionId = url.searchParams.get('sessionId');
10
+
11
+ if (!sessionId) {
12
+ return new Response('sessionId parameter required', { status: 400 });
13
+ }
14
+
15
+ const filePath = getSessionFilePath(decodeURIComponent(projectName), sessionId);
16
+ if (!filePath) {
17
+ return new Response('Session file not found', { status: 404 });
18
+ }
19
+
20
+ const encoder = new TextEncoder();
21
+ let cleanup: (() => void) | null = null;
22
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
23
+ let closed = false;
24
+
25
+ const stream = new ReadableStream({
26
+ start(controller) {
27
+ // Send all existing entries
28
+ const existing = readSessionEntries(filePath);
29
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'init', entries: existing })}\n\n`));
30
+
31
+ // Heartbeat
32
+ heartbeat = setInterval(() => {
33
+ if (!closed) {
34
+ try { controller.enqueue(encoder.encode(': heartbeat\n\n')); } catch { doCleanup(); }
35
+ }
36
+ }, 15000);
37
+
38
+ // Tail for new entries
39
+ cleanup = tailSessionFile(
40
+ filePath,
41
+ (entries) => {
42
+ if (closed) return;
43
+ try {
44
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'update', entries })}\n\n`));
45
+ } catch { doCleanup(); }
46
+ },
47
+ () => {
48
+ doCleanup();
49
+ try { controller.close(); } catch {}
50
+ },
51
+ );
52
+ },
53
+ cancel() {
54
+ doCleanup();
55
+ },
56
+ });
57
+
58
+ function doCleanup() {
59
+ closed = true;
60
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
61
+ if (cleanup) { cleanup(); cleanup = null; }
62
+ }
63
+
64
+ return new Response(stream, {
65
+ headers: {
66
+ 'Content-Type': 'text/event-stream',
67
+ 'Cache-Control': 'no-cache, no-transform',
68
+ Connection: 'keep-alive',
69
+ 'X-Accel-Buffering': 'no',
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,37 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { listClaudeSessions, deleteSession } from '@/lib/claude-sessions';
3
+ import { getDb } from '@/src/core/db/database';
4
+ import { getDbPath } from '@/src/config';
5
+
6
+ export async function GET(_req: Request, { params }: { params: Promise<{ projectName: string }> }) {
7
+ const { projectName } = await params;
8
+ const sessions = listClaudeSessions(decodeURIComponent(projectName));
9
+ return NextResponse.json(sessions);
10
+ }
11
+
12
+ export async function DELETE(req: Request, { params }: { params: Promise<{ projectName: string }> }) {
13
+ const { projectName } = await params;
14
+ const project = decodeURIComponent(projectName);
15
+ const body = await req.json();
16
+
17
+ // Support both single sessionId and batch sessionIds
18
+ const ids: string[] = body.sessionIds || (body.sessionId ? [body.sessionId] : []);
19
+
20
+ if (ids.length === 0) {
21
+ return NextResponse.json({ error: 'sessionId or sessionIds required' }, { status: 400 });
22
+ }
23
+
24
+ const db = getDb(getDbPath());
25
+ let deletedCount = 0;
26
+
27
+ for (const id of ids) {
28
+ if (deleteSession(project, id)) {
29
+ deletedCount++;
30
+ }
31
+ try {
32
+ db.prepare('DELETE FROM cached_sessions WHERE project_name = ? AND session_id = ?').run(project, id);
33
+ } catch {}
34
+ }
35
+
36
+ return NextResponse.json({ ok: true, deleted: deletedCount });
37
+ }
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { ensureInitialized } from '@/lib/init';
3
+ import { syncSessionsToDb, getAllCachedSessions } from '@/lib/session-watcher';
4
+
5
+ export async function GET() {
6
+ ensureInitialized();
7
+ const all = getAllCachedSessions();
8
+ return NextResponse.json(all);
9
+ }
10
+
11
+ export async function POST(req: Request) {
12
+ ensureInitialized();
13
+ const body = await req.json().catch(() => ({}));
14
+ const count = syncSessionsToDb(body.projectName);
15
+ const all = getAllCachedSessions();
16
+ return NextResponse.json({ synced: count, sessions: all });
17
+ }
@@ -0,0 +1,6 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { listFlows } from '@/lib/flows';
3
+
4
+ export async function GET() {
5
+ return NextResponse.json(listFlows());
6
+ }
@@ -0,0 +1,19 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { runFlow } from '@/lib/flows';
3
+
4
+ export async function POST(req: Request) {
5
+ const { name } = await req.json();
6
+ if (!name) {
7
+ return NextResponse.json({ error: 'Flow name required' }, { status: 400 });
8
+ }
9
+
10
+ try {
11
+ const result = runFlow(name);
12
+ return NextResponse.json({
13
+ flow: result.flow.name,
14
+ tasks: result.tasks.map(t => ({ id: t.id, projectName: t.projectName, prompt: t.prompt })),
15
+ });
16
+ } catch (err: any) {
17
+ return NextResponse.json({ error: err.message }, { status: 400 });
18
+ }
19
+ }
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { loadSettings } from '@/lib/settings';
3
+
4
+ export async function POST() {
5
+ const settings = loadSettings();
6
+ const { telegramBotToken, telegramChatId } = settings;
7
+
8
+ if (!telegramBotToken || !telegramChatId) {
9
+ return NextResponse.json({ ok: false, error: 'Telegram bot token or chat ID not configured' });
10
+ }
11
+
12
+ try {
13
+ const url = `https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
14
+ const res = await fetch(url, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({
18
+ chat_id: telegramChatId,
19
+ text: '✅ *Forge* — Test notification!\n\nTelegram notifications are working.',
20
+ parse_mode: 'Markdown',
21
+ }),
22
+ });
23
+
24
+ if (!res.ok) {
25
+ const body = await res.text();
26
+ return NextResponse.json({ ok: false, error: body });
27
+ }
28
+
29
+ return NextResponse.json({ ok: true });
30
+ } catch (err: any) {
31
+ return NextResponse.json({ ok: false, error: err.message });
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { scanProjects } from '@/lib/projects';
3
+
4
+ export async function GET() {
5
+ const projects = scanProjects();
6
+ return NextResponse.json(projects);
7
+ }
@@ -0,0 +1,64 @@
1
+ import { getSessionManager } from '@/lib/session-manager';
2
+ import { chatStream } from '@/src/core/providers/chat';
3
+ import type { ModelMessage } from 'ai';
4
+
5
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params;
7
+ const { message } = await req.json();
8
+ const manager = getSessionManager();
9
+
10
+ const session = manager.get(id);
11
+ if (!session) {
12
+ return new Response(JSON.stringify({ error: 'Session not found' }), { status: 404 });
13
+ }
14
+
15
+ // Save user message
16
+ manager.addMessage(id, 'user', message, session.provider, session.model);
17
+
18
+ // Get memory-filtered messages
19
+ const memoryMessages = manager.getMemoryMessages(id);
20
+ const coreMessages: ModelMessage[] = memoryMessages.map(m => ({
21
+ role: m.role as 'user' | 'assistant',
22
+ content: m.content,
23
+ }));
24
+
25
+ manager.updateStatus(id, 'running');
26
+
27
+ // Stream response
28
+ const encoder = new TextEncoder();
29
+ const stream = new ReadableStream({
30
+ async start(controller) {
31
+ try {
32
+ const result = await chatStream({
33
+ provider: session.provider,
34
+ model: session.model || undefined,
35
+ systemPrompt: session.systemPrompt,
36
+ messages: coreMessages,
37
+ onToken(token) {
38
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ token })}\n\n`));
39
+ },
40
+ });
41
+
42
+ // Save assistant message
43
+ manager.addMessage(id, 'assistant', result.content, result.provider, result.model);
44
+ manager.recordUsage(id, result);
45
+ manager.updateStatus(id, 'idle');
46
+
47
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true, usage: { input: result.inputTokens, output: result.outputTokens } })}\n\n`));
48
+ controller.close();
49
+ } catch (err: any) {
50
+ manager.updateStatus(id, 'error');
51
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: err.message })}\n\n`));
52
+ controller.close();
53
+ }
54
+ },
55
+ });
56
+
57
+ return new Response(stream, {
58
+ headers: {
59
+ 'Content-Type': 'text/event-stream',
60
+ 'Cache-Control': 'no-cache',
61
+ Connection: 'keep-alive',
62
+ },
63
+ });
64
+ }
@@ -0,0 +1,9 @@
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 messages = manager.getMessages(id);
8
+ return NextResponse.json(messages);
9
+ }