@aion0/forge 0.4.16 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/RELEASE_NOTES.md +21 -14
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +13 -1
- package/check-forge-status.sh +9 -0
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +10 -2
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +11 -6
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +31 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +257 -43
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2245 -0
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +7 -1
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +89 -0
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +254 -0
- package/lib/help-docs/CLAUDE.md +7 -2
- package/lib/init.ts +60 -10
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1914 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +814 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +4 -1
- package/src/config/index.ts +12 -1
- package/src/core/db/database.ts +1 -0
- package/start.sh +7 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="app/icon.
|
|
2
|
+
<img src="app/icon.ico" width="80" height="80" alt="Forge">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Forge</h1>
|
|
@@ -49,11 +49,36 @@ Forge turns Claude Code into a remote-accessible coding platform. Run it on your
|
|
|
49
49
|
|
|
50
50
|
No API keys required. Uses your existing Claude Code subscription. Code never leaves your machine.
|
|
51
51
|
|
|
52
|
+
## Multi-Agent Workspace <sup>v0.5.0</sup>
|
|
53
|
+
|
|
54
|
+
Define agent teams with roles, dependencies, and steps. Agents run as daemons and communicate via a structured message bus.
|
|
55
|
+
|
|
56
|
+
```mermaid
|
|
57
|
+
graph LR
|
|
58
|
+
Input["📋 Requirements"] --> PM["🎯 PM"]
|
|
59
|
+
PM --> Eng["🔨 Engineer"]
|
|
60
|
+
Eng --> QA["🧪 QA"]
|
|
61
|
+
Eng --> Rev["👁 Reviewer"]
|
|
62
|
+
QA --> Rev
|
|
63
|
+
|
|
64
|
+
style Input fill:#f0883e,stroke:#f0883e,color:#fff
|
|
65
|
+
style PM fill:#a371f7,stroke:#a371f7,color:#fff
|
|
66
|
+
style Eng fill:#58a6ff,stroke:#58a6ff,color:#fff
|
|
67
|
+
style QA fill:#3fb950,stroke:#3fb950,color:#fff
|
|
68
|
+
style Rev fill:#f778ba,stroke:#f778ba,color:#fff
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **Agent profiles** — Reusable configs with env vars, model overrides, custom API endpoints (Claude, Codex, Aider)
|
|
72
|
+
- **Message system** — Notifications (follow DAG) + Tickets (any direction, retry limits) with causedBy tracing
|
|
73
|
+
- **Manual mode** — Open a terminal on any agent, work interactively, return to auto
|
|
74
|
+
- **Watch manager** — Monitor files, git, or commands. Auto-analyze or require approval on changes
|
|
75
|
+
|
|
52
76
|
## Features
|
|
53
77
|
|
|
54
78
|
| | |
|
|
55
79
|
|---|---|
|
|
56
|
-
| **
|
|
80
|
+
| **Multi-Agent Workspace** | Agent teams with DAG dependencies, message bus, watch monitoring, manual/auto mode |
|
|
81
|
+
| **Vibe Coding** | Browser tmux terminal, multi-tab, split panes, WebGL, Ctrl+F search |
|
|
57
82
|
| **AI Tasks** | Background Claude Code execution with live streaming output |
|
|
58
83
|
| **Pipelines** | YAML DAG workflows with parallel execution & visual editor |
|
|
59
84
|
| **Remote Access** | Cloudflare Tunnel with 2FA (password + session code) |
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
|
-
# Forge v0.
|
|
1
|
+
# Forge v0.5.1
|
|
2
2
|
|
|
3
|
-
Released: 2026-03-
|
|
3
|
+
Released: 2026-03-28
|
|
4
4
|
|
|
5
|
-
## Changes since v0.
|
|
5
|
+
## Changes since v0.5.0
|
|
6
6
|
|
|
7
7
|
### Features
|
|
8
|
-
- feat:
|
|
9
|
-
- feat:
|
|
10
|
-
- Revert "feat: mobile real-time streaming with tool activity display"
|
|
11
|
-
- feat: mobile real-time streaming with tool activity display
|
|
8
|
+
- feat: daemon health check + SSE for all message actions
|
|
9
|
+
- feat: pending_approval status for watch approve + requiresApproval
|
|
12
10
|
|
|
13
11
|
### Bug Fixes
|
|
14
|
-
- fix:
|
|
15
|
-
- fix:
|
|
16
|
-
- fix:
|
|
17
|
-
- fix:
|
|
12
|
+
- fix: stop old worker before creating new one in enterDaemonListening
|
|
13
|
+
- fix: prevent multiple running messages + clean stale running
|
|
14
|
+
- fix: requiresApproval set at message arrival, not in message loop
|
|
15
|
+
- fix: approved messages not re-converted to pending_approval
|
|
16
|
+
- fix: message loop never stops + auto-recreate dead workers
|
|
17
|
+
- fix: approve/reject emit SSE events + reject marks as failed
|
|
18
|
+
- fix: emit bus_message_status after watch approve sets pending_approval
|
|
19
|
+
- fix: pending_approval edge cases
|
|
20
|
+
- fix: system messages (_watch, _system, user) bypass causedBy rules
|
|
18
21
|
|
|
19
22
|
### Documentation
|
|
20
|
-
- docs:
|
|
23
|
+
- docs: add workspace section + agent flow diagram to README
|
|
24
|
+
- docs: README with Mermaid diagrams for v0.5.0
|
|
25
|
+
- docs: update README for v0.5.0 — multi-agent workspace
|
|
26
|
+
- docs: update workspace help with watch, logs, forge skills, send protection
|
|
21
27
|
|
|
22
28
|
### Other
|
|
23
|
-
-
|
|
29
|
+
- ui: add requiresApproval toggle in agent config modal
|
|
30
|
+
- debug: log watch analyze skip reasons
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.
|
|
33
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.0...v0.5.1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { listAgents, getDefaultAgentId, resolveTerminalLaunch } from '@/lib/agents';
|
|
3
|
+
|
|
4
|
+
export async function GET(req: Request) {
|
|
5
|
+
const url = new URL(req.url);
|
|
6
|
+
const resolve = url.searchParams.get('resolve');
|
|
7
|
+
|
|
8
|
+
// GET /api/agents?resolve=claude → resolve terminal launch info for an agent
|
|
9
|
+
if (resolve) {
|
|
10
|
+
const info = resolveTerminalLaunch(resolve);
|
|
11
|
+
return NextResponse.json(info);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const agents = listAgents();
|
|
15
|
+
const defaultAgent = getDefaultAgentId();
|
|
16
|
+
return NextResponse.json({ agents, defaultAgent });
|
|
17
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import {
|
|
3
|
+
getDelivery, cancelDelivery, deleteDelivery,
|
|
4
|
+
approveDeliveryPhase, rejectDeliveryPhase,
|
|
5
|
+
sendToAgent, retryPhase,
|
|
6
|
+
} from '@/lib/delivery';
|
|
7
|
+
import { listArtifacts, getArtifact } from '@/lib/artifacts';
|
|
8
|
+
import type { PhaseName } from '@/lib/delivery';
|
|
9
|
+
|
|
10
|
+
// GET /api/delivery/:id — get delivery state + artifacts
|
|
11
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
12
|
+
const { id } = await params;
|
|
13
|
+
const delivery = getDelivery(id);
|
|
14
|
+
if (!delivery) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
15
|
+
|
|
16
|
+
const artifacts = listArtifacts(id);
|
|
17
|
+
return NextResponse.json({ ...delivery, artifacts });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// POST /api/delivery/:id — actions
|
|
21
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
22
|
+
const { id } = await params;
|
|
23
|
+
const body = await req.json();
|
|
24
|
+
const { action } = body;
|
|
25
|
+
|
|
26
|
+
if (action === 'cancel') {
|
|
27
|
+
return NextResponse.json({ ok: cancelDelivery(id) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (action === 'delete') {
|
|
31
|
+
return NextResponse.json({ ok: deleteDelivery(id) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (action === 'approve') {
|
|
35
|
+
return NextResponse.json({ ok: approveDeliveryPhase(id, body.feedback) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (action === 'reject') {
|
|
39
|
+
if (!body.feedback) return NextResponse.json({ error: 'feedback required' }, { status: 400 });
|
|
40
|
+
return NextResponse.json({ ok: rejectDeliveryPhase(id, body.feedback) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (action === 'send') {
|
|
44
|
+
const { phase, message } = body;
|
|
45
|
+
if (!phase || !message) return NextResponse.json({ error: 'phase and message required' }, { status: 400 });
|
|
46
|
+
return NextResponse.json({ ok: sendToAgent(id, phase as PhaseName, message) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (action === 'retry') {
|
|
50
|
+
const { phase } = body;
|
|
51
|
+
if (!phase) return NextResponse.json({ error: 'phase required' }, { status: 400 });
|
|
52
|
+
return NextResponse.json({ ok: retryPhase(id, phase as PhaseName) });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// DELETE /api/delivery/:id
|
|
59
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
60
|
+
const { id } = await params;
|
|
61
|
+
return NextResponse.json({ ok: deleteDelivery(id) });
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createDelivery, listDeliveries, ROLE_PRESETS } from '@/lib/delivery';
|
|
3
|
+
|
|
4
|
+
// GET /api/delivery — list deliveries, or get role presets
|
|
5
|
+
export async function GET(req: Request) {
|
|
6
|
+
const { searchParams } = new URL(req.url);
|
|
7
|
+
|
|
8
|
+
if (searchParams.get('type') === 'presets') {
|
|
9
|
+
return NextResponse.json(ROLE_PRESETS);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const deliveries = listDeliveries();
|
|
13
|
+
return NextResponse.json(deliveries);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// POST /api/delivery — create new delivery
|
|
17
|
+
export async function POST(req: Request) {
|
|
18
|
+
const body = await req.json();
|
|
19
|
+
const { title, project, projectPath, prUrl, description, agentId, phases } = body;
|
|
20
|
+
|
|
21
|
+
if (!project || !projectPath) {
|
|
22
|
+
return NextResponse.json({ error: 'project and projectPath required' }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const delivery = createDelivery({
|
|
27
|
+
title: title || description?.slice(0, 50) || 'Delivery',
|
|
28
|
+
project,
|
|
29
|
+
projectPath,
|
|
30
|
+
prUrl,
|
|
31
|
+
description,
|
|
32
|
+
agentId,
|
|
33
|
+
customPhases: phases, // user-defined phases
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return NextResponse.json(delivery);
|
|
37
|
+
} catch (e: any) {
|
|
38
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -7,26 +7,32 @@ export const runtime = 'nodejs';
|
|
|
7
7
|
|
|
8
8
|
// POST /api/mobile-chat — send a message to claude and stream response
|
|
9
9
|
export async function POST(req: Request) {
|
|
10
|
-
const { message, projectPath, resume } = await req.json() as {
|
|
10
|
+
const { message, projectPath, resume, agent: agentId } = await req.json() as {
|
|
11
11
|
message: string;
|
|
12
12
|
projectPath: string;
|
|
13
13
|
resume?: boolean;
|
|
14
|
+
agent?: string;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
if (!message || !projectPath) {
|
|
17
18
|
return NextResponse.json({ error: 'message and projectPath required' }, { status: 400 });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
21
|
+
const { getAgent } = require('@/lib/agents');
|
|
22
|
+
const adapter = getAgent(agentId);
|
|
22
23
|
const projectName = projectPath.split('/').pop() || projectPath;
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const spawnOpts = adapter.buildTaskSpawn({
|
|
26
|
+
projectPath,
|
|
27
|
+
prompt: message,
|
|
28
|
+
skipPermissions: true,
|
|
29
|
+
outputFormat: adapter.config.capabilities?.supportsStreamJson ? 'json' : undefined,
|
|
30
|
+
conversationId: resume ? 'last' : undefined,
|
|
31
|
+
});
|
|
26
32
|
|
|
27
|
-
const child = spawn(
|
|
33
|
+
const child = spawn(spawnOpts.cmd, spawnOpts.args, {
|
|
28
34
|
cwd: projectPath,
|
|
29
|
-
env: { ...process.env },
|
|
35
|
+
env: { ...process.env, ...(spawnOpts.env || {}) },
|
|
30
36
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
37
|
});
|
|
32
38
|
|
package/app/api/monitor/route.ts
CHANGED
|
@@ -7,11 +7,13 @@ function run(cmd: string): string {
|
|
|
7
7
|
} catch { return ''; }
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function countProcess(pattern: string): { count: number; pid: string } {
|
|
10
|
+
function countProcess(pattern: string): { count: number; pid: string; startedAt: string } {
|
|
11
11
|
const out = run(`ps aux | grep '${pattern}' | grep -v grep | head -1`);
|
|
12
12
|
const pid = out ? out.split(/\s+/)[1] || '' : '';
|
|
13
13
|
const count = out ? run(`ps aux | grep '${pattern}' | grep -v grep | wc -l`).trim() : '0';
|
|
14
|
-
|
|
14
|
+
// Get start time from ps
|
|
15
|
+
const startedAt = pid ? run(`ps -o lstart= -p ${pid} 2>/dev/null`).trim() : '';
|
|
16
|
+
return { count: parseInt(count), pid, startedAt };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export async function GET() {
|
|
@@ -19,6 +21,7 @@ export async function GET() {
|
|
|
19
21
|
const nextjs = countProcess('next-server');
|
|
20
22
|
const terminal = countProcess('terminal-standalone');
|
|
21
23
|
const telegram = countProcess('telegram-standalone');
|
|
24
|
+
const workspace = countProcess('workspace-standalone');
|
|
22
25
|
const tunnel = countProcess('cloudflared tunnel');
|
|
23
26
|
|
|
24
27
|
// Tunnel URL
|
|
@@ -47,10 +50,11 @@ export async function GET() {
|
|
|
47
50
|
|
|
48
51
|
return NextResponse.json({
|
|
49
52
|
processes: {
|
|
50
|
-
nextjs: { running: nextjs.count > 0, pid: nextjs.pid },
|
|
51
|
-
terminal: { running: terminal.count > 0, pid: terminal.pid },
|
|
52
|
-
telegram: { running: telegram.count > 0, pid: telegram.pid },
|
|
53
|
-
|
|
53
|
+
nextjs: { running: nextjs.count > 0, pid: nextjs.pid, startedAt: nextjs.startedAt },
|
|
54
|
+
terminal: { running: terminal.count > 0, pid: terminal.pid, startedAt: terminal.startedAt },
|
|
55
|
+
telegram: { running: telegram.count > 0, pid: telegram.pid, startedAt: telegram.startedAt },
|
|
56
|
+
workspace: { running: workspace.count > 0, pid: workspace.pid, startedAt: workspace.startedAt },
|
|
57
|
+
tunnel: { running: tunnel.count > 0, pid: tunnel.pid, url: tunnelUrl, startedAt: tunnel.startedAt },
|
|
54
58
|
},
|
|
55
59
|
sessions,
|
|
56
60
|
uptime: uptime.replace(/.*up\s+/, '').replace(/,\s+\d+ user.*/, '').trim(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import { getPipeline, cancelPipeline, deletePipeline } from '@/lib/pipeline';
|
|
2
|
+
import { getPipeline, cancelPipeline, deletePipeline, injectConversationMessage } from '@/lib/pipeline';
|
|
3
3
|
|
|
4
4
|
// GET /api/pipelines/:id
|
|
5
5
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -9,10 +9,11 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
9
9
|
return NextResponse.json(pipeline);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// POST /api/pipelines/:id — actions (cancel)
|
|
12
|
+
// POST /api/pipelines/:id — actions (cancel, delete, inject)
|
|
13
13
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
14
|
const { id } = await params;
|
|
15
|
-
const
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const { action } = body;
|
|
16
17
|
|
|
17
18
|
if (action === 'cancel') {
|
|
18
19
|
const ok = cancelPipeline(id);
|
|
@@ -24,5 +25,17 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
24
25
|
return NextResponse.json({ ok });
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// Inject a message into a running conversation
|
|
29
|
+
if (action === 'inject') {
|
|
30
|
+
const { agentId, message } = body;
|
|
31
|
+
if (!agentId || !message) return NextResponse.json({ error: 'agentId and message required' }, { status: 400 });
|
|
32
|
+
try {
|
|
33
|
+
const ok = injectConversationMessage(id, agentId, message);
|
|
34
|
+
return NextResponse.json({ ok });
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
return NextResponse.json({ error: e.message }, { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
|
28
41
|
}
|
package/app/api/tasks/route.ts
CHANGED
|
@@ -14,7 +14,7 @@ export async function GET(req: Request) {
|
|
|
14
14
|
|
|
15
15
|
// Create a new task
|
|
16
16
|
export async function POST(req: Request) {
|
|
17
|
-
const { projectName, prompt, priority, newSession, conversationId, scheduledAt, mode, watchConfig } = await req.json();
|
|
17
|
+
const { projectName, prompt, priority, newSession, conversationId, scheduledAt, mode, watchConfig, agent } = await req.json();
|
|
18
18
|
|
|
19
19
|
if (!projectName || !prompt) {
|
|
20
20
|
return NextResponse.json({ error: 'projectName and prompt are required' }, { status: 400 });
|
|
@@ -37,6 +37,7 @@ export async function POST(req: Request) {
|
|
|
37
37
|
scheduledAt: scheduledAt || undefined,
|
|
38
38
|
mode: mode || 'prompt',
|
|
39
39
|
watchConfig: watchConfig || undefined,
|
|
40
|
+
agent: agent || undefined,
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
return NextResponse.json(task);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const WORKSPACE_PORT = Number(process.env.WORKSPACE_PORT) || 8405;
|
|
4
|
+
const DAEMON_URL = `http://localhost:${WORKSPACE_PORT}`;
|
|
5
|
+
|
|
6
|
+
// Proxy to workspace daemon — Agent operations
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
const body = await req.json();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${DAEMON_URL}/workspace/${id}/agents`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify(body),
|
|
16
|
+
});
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
return NextResponse.json(data, { status: res.status });
|
|
19
|
+
} catch (err: any) {
|
|
20
|
+
return NextResponse.json({ error: 'Workspace daemon not available' }, { status: 503 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Proxy to workspace daemon — Get agent states
|
|
25
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
26
|
+
const { id } = await params;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${DAEMON_URL}/workspace/${id}/agents`);
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
return NextResponse.json(data, { status: res.status });
|
|
32
|
+
} catch (err: any) {
|
|
33
|
+
return NextResponse.json({ error: 'Workspace daemon not available' }, { status: 503 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const WORKSPACE_PORT = Number(process.env.WORKSPACE_PORT) || 8405;
|
|
4
|
+
const DAEMON_URL = `http://localhost:${WORKSPACE_PORT}`;
|
|
5
|
+
|
|
6
|
+
// Proxy to workspace daemon — Memory query
|
|
7
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id: workspaceId } = await params;
|
|
9
|
+
const url = new URL(req.url);
|
|
10
|
+
const agentId = url.searchParams.get('agentId');
|
|
11
|
+
|
|
12
|
+
if (!agentId) {
|
|
13
|
+
return NextResponse.json({ error: 'agentId required' }, { status: 400 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`${DAEMON_URL}/workspace/${workspaceId}/memory?agentId=${encodeURIComponent(agentId)}`);
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
return NextResponse.json(data, { status: res.status });
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
return NextResponse.json({ error: 'Workspace daemon not available' }, { status: 503 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const WORKSPACE_PORT = Number(process.env.WORKSPACE_PORT) || 8405;
|
|
4
|
+
const DAEMON_URL = `http://localhost:${WORKSPACE_PORT}`;
|
|
5
|
+
|
|
6
|
+
// Proxy to workspace daemon — Smith API (called by forge skills in terminal)
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
const body = await req.json();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${DAEMON_URL}/workspace/${id}/smith`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify(body),
|
|
16
|
+
});
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
return NextResponse.json(data, { status: res.status });
|
|
19
|
+
} catch (err: any) {
|
|
20
|
+
return NextResponse.json({ error: 'Workspace daemon not available' }, { status: 503 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const WORKSPACE_PORT = Number(process.env.WORKSPACE_PORT) || 8405;
|
|
2
|
+
const DAEMON_URL = `http://localhost:${WORKSPACE_PORT}`;
|
|
3
|
+
|
|
4
|
+
// SSE relay — proxy daemon's SSE stream to browser
|
|
5
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
|
+
const { id } = await params;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const daemonRes = await fetch(`${DAEMON_URL}/workspace/${id}/stream`, {
|
|
10
|
+
signal: req.signal,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!daemonRes.ok || !daemonRes.body) {
|
|
14
|
+
return new Response(daemonRes.statusText || 'Daemon error', { status: daemonRes.status });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Pipe daemon SSE stream directly to browser
|
|
18
|
+
return new Response(daemonRes.body, {
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'text/event-stream',
|
|
21
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
22
|
+
'Connection': 'keep-alive',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
return new Response('Workspace daemon not available', { status: 503 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { listWorkspaces, findWorkspaceByProject, loadWorkspace, saveWorkspace, deleteWorkspace } from '@/lib/workspace';
|
|
3
|
+
import type { WorkspaceState } from '@/lib/workspace';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// List workspaces, find by projectPath, or export template
|
|
7
|
+
export async function GET(req: Request) {
|
|
8
|
+
const url = new URL(req.url);
|
|
9
|
+
const projectPath = url.searchParams.get('projectPath');
|
|
10
|
+
const exportId = url.searchParams.get('export');
|
|
11
|
+
|
|
12
|
+
if (exportId) {
|
|
13
|
+
// Export workspace as template (agents + positions, no state/logs)
|
|
14
|
+
const ws = loadWorkspace(exportId);
|
|
15
|
+
if (!ws) return NextResponse.json({ error: 'not found' }, { status: 404 });
|
|
16
|
+
const template = {
|
|
17
|
+
name: ws.projectName + ' template',
|
|
18
|
+
agents: ws.agents.map(a => ({ ...a, entries: undefined })), // strip Input entries
|
|
19
|
+
nodePositions: ws.nodePositions,
|
|
20
|
+
exportedAt: Date.now(),
|
|
21
|
+
};
|
|
22
|
+
return NextResponse.json(template);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (projectPath) {
|
|
26
|
+
const ws = findWorkspaceByProject(projectPath);
|
|
27
|
+
return NextResponse.json(ws || null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return NextResponse.json(listWorkspaces());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create workspace or import template
|
|
34
|
+
export async function POST(req: Request) {
|
|
35
|
+
const body = await req.json();
|
|
36
|
+
const { projectPath, projectName, template } = body;
|
|
37
|
+
|
|
38
|
+
if (!projectPath || !projectName) {
|
|
39
|
+
return NextResponse.json({ error: 'projectPath and projectName are required' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existing = findWorkspaceByProject(projectPath);
|
|
43
|
+
if (existing && !template) {
|
|
44
|
+
return NextResponse.json(existing);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const state: WorkspaceState = {
|
|
48
|
+
id: existing?.id || randomUUID(),
|
|
49
|
+
projectPath,
|
|
50
|
+
projectName,
|
|
51
|
+
agents: [],
|
|
52
|
+
agentStates: {},
|
|
53
|
+
nodePositions: {},
|
|
54
|
+
busLog: [],
|
|
55
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
56
|
+
updatedAt: Date.now(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Import template: create agents from template with new IDs
|
|
60
|
+
if (template?.agents) {
|
|
61
|
+
const idMap = new Map<string, string>(); // old ID → new ID
|
|
62
|
+
const ts = Date.now();
|
|
63
|
+
for (const agent of template.agents) {
|
|
64
|
+
const newId = `${agent.label.toLowerCase().replace(/\s+/g, '-')}-${ts}-${Math.random().toString(36).slice(2, 5)}`;
|
|
65
|
+
idMap.set(agent.id, newId);
|
|
66
|
+
}
|
|
67
|
+
for (const agent of template.agents) {
|
|
68
|
+
state.agents.push({
|
|
69
|
+
...agent,
|
|
70
|
+
id: idMap.get(agent.id) || agent.id,
|
|
71
|
+
dependsOn: agent.dependsOn.map((d: string) => idMap.get(d) || d),
|
|
72
|
+
entries: agent.type === 'input' ? [] : undefined,
|
|
73
|
+
});
|
|
74
|
+
state.agentStates[idMap.get(agent.id) || agent.id] = { smithStatus: 'down', mode: 'auto', taskStatus: 'idle', history: [], artifacts: [] };
|
|
75
|
+
}
|
|
76
|
+
if (template.nodePositions) {
|
|
77
|
+
for (const [oldId, pos] of Object.entries(template.nodePositions)) {
|
|
78
|
+
const newId = idMap.get(oldId);
|
|
79
|
+
if (newId) state.nodePositions[newId] = pos as { x: number; y: number };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await saveWorkspace(state);
|
|
85
|
+
return NextResponse.json(state, { status: 201 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Delete a workspace
|
|
89
|
+
export async function DELETE(req: Request) {
|
|
90
|
+
const url = new URL(req.url);
|
|
91
|
+
const id = url.searchParams.get('id');
|
|
92
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
|
|
93
|
+
|
|
94
|
+
// Unload from daemon if active
|
|
95
|
+
const daemonUrl = `http://localhost:${Number(process.env.WORKSPACE_PORT) || 8405}`;
|
|
96
|
+
try { await fetch(`${daemonUrl}/workspace/${id}/unload`, { method: 'POST' }); } catch {}
|
|
97
|
+
|
|
98
|
+
deleteWorkspace(id);
|
|
99
|
+
return NextResponse.json({ ok: true });
|
|
100
|
+
}
|
package/app/global-error.tsx
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
|
|
3
|
+
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
4
4
|
return (
|
|
5
|
-
<html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charSet="utf-8" />
|
|
8
|
+
</head>
|
|
6
9
|
<body style={{ background: '#0a0a0a', color: '#e5e5e5', fontFamily: 'monospace', padding: '2rem' }}>
|
|
7
10
|
<h2>Something went wrong</h2>
|
|
8
|
-
<p style={{ color: '#999' }}>{error
|
|
9
|
-
<button
|
|
11
|
+
<p style={{ color: '#999' }}>{error?.message || 'Unknown error'}</p>
|
|
12
|
+
<button
|
|
13
|
+
onClick={() => reset()}
|
|
14
|
+
style={{ marginTop: '1rem', padding: '0.5rem 1rem', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
|
15
|
+
>
|
|
10
16
|
Try again
|
|
11
17
|
</button>
|
|
12
18
|
</body>
|
package/app/icon.ico
ADDED
|
Binary file
|
package/app/layout.tsx
CHANGED
|
@@ -8,8 +8,8 @@ export const metadata: Metadata = {
|
|
|
8
8
|
|
|
9
9
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
10
|
return (
|
|
11
|
-
<html lang="en" className="dark">
|
|
12
|
-
<body className="min-h-screen bg-[var(--bg-primary)]">
|
|
11
|
+
<html lang="en" className="dark" suppressHydrationWarning>
|
|
12
|
+
<body className="min-h-screen bg-[var(--bg-primary)]" suppressHydrationWarning>
|
|
13
13
|
{children}
|
|
14
14
|
</body>
|
|
15
15
|
</html>
|