@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.
- package/CLAUDE.md +4 -0
- package/README.md +264 -0
- package/app/api/auth/[...nextauth]/route.ts +3 -0
- package/app/api/claude/[id]/route.ts +31 -0
- package/app/api/claude/[id]/stream/route.ts +63 -0
- package/app/api/claude/route.ts +28 -0
- package/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/app/api/claude-sessions/sync/route.ts +17 -0
- package/app/api/flows/route.ts +6 -0
- package/app/api/flows/run/route.ts +19 -0
- package/app/api/notify/test/route.ts +33 -0
- package/app/api/projects/route.ts +7 -0
- package/app/api/sessions/[id]/chat/route.ts +64 -0
- package/app/api/sessions/[id]/messages/route.ts +9 -0
- package/app/api/sessions/[id]/route.ts +17 -0
- package/app/api/sessions/route.ts +20 -0
- package/app/api/settings/route.ts +15 -0
- package/app/api/status/route.ts +12 -0
- package/app/api/tasks/[id]/route.ts +36 -0
- package/app/api/tasks/[id]/stream/route.ts +77 -0
- package/app/api/tasks/link/route.ts +37 -0
- package/app/api/tasks/route.ts +43 -0
- package/app/api/tasks/session/route.ts +14 -0
- package/app/api/templates/route.ts +6 -0
- package/app/api/tunnel/route.ts +20 -0
- package/app/api/watchers/route.ts +33 -0
- package/app/globals.css +26 -0
- package/app/icon.svg +26 -0
- package/app/layout.tsx +17 -0
- package/app/login/page.tsx +61 -0
- package/app/page.tsx +9 -0
- package/cli/mw.ts +377 -0
- package/components/ChatPanel.tsx +191 -0
- package/components/ClaudeTerminal.tsx +267 -0
- package/components/Dashboard.tsx +270 -0
- package/components/MarkdownContent.tsx +57 -0
- package/components/NewSessionModal.tsx +93 -0
- package/components/NewTaskModal.tsx +456 -0
- package/components/ProjectList.tsx +108 -0
- package/components/SessionList.tsx +74 -0
- package/components/SessionView.tsx +655 -0
- package/components/SettingsModal.tsx +366 -0
- package/components/StatusBar.tsx +99 -0
- package/components/TaskBoard.tsx +110 -0
- package/components/TaskDetail.tsx +351 -0
- package/components/TunnelToggle.tsx +163 -0
- package/components/WebTerminal.tsx +1069 -0
- package/docs/LOCAL-DEPLOY.md +144 -0
- package/docs/roadmap-multi-agent-workflow.md +330 -0
- package/instrumentation.ts +14 -0
- package/lib/auth.ts +47 -0
- package/lib/claude-process.ts +352 -0
- package/lib/claude-sessions.ts +267 -0
- package/lib/cloudflared.ts +218 -0
- package/lib/flows.ts +86 -0
- package/lib/init.ts +82 -0
- package/lib/notify.ts +75 -0
- package/lib/password.ts +77 -0
- package/lib/projects.ts +86 -0
- package/lib/session-manager.ts +156 -0
- package/lib/session-watcher.ts +345 -0
- package/lib/settings.ts +44 -0
- package/lib/task-manager.ts +668 -0
- package/lib/telegram-bot.ts +912 -0
- package/lib/terminal-server.ts +70 -0
- package/lib/terminal-standalone.ts +363 -0
- package/middleware.ts +33 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +16 -0
- package/package.json +66 -0
- package/postcss.config.mjs +7 -0
- package/src/config/index.ts +119 -0
- package/src/core/db/database.ts +133 -0
- package/src/core/memory/strategy.ts +32 -0
- package/src/core/providers/chat.ts +65 -0
- package/src/core/providers/registry.ts +60 -0
- package/src/core/session/manager.ts +190 -0
- package/src/types/index.ts +128 -0
- package/tsconfig.json +41 -0
package/CLAUDE.md
ADDED
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,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,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,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
|
+
}
|