@aion0/forge 0.4.16 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +27 -2
  2. package/RELEASE_NOTES.md +21 -14
  3. package/app/api/agents/route.ts +17 -0
  4. package/app/api/delivery/[id]/route.ts +62 -0
  5. package/app/api/delivery/route.ts +40 -0
  6. package/app/api/mobile-chat/route.ts +13 -7
  7. package/app/api/monitor/route.ts +10 -6
  8. package/app/api/pipelines/[id]/route.ts +16 -3
  9. package/app/api/tasks/route.ts +2 -1
  10. package/app/api/workspace/[id]/agents/route.ts +35 -0
  11. package/app/api/workspace/[id]/memory/route.ts +23 -0
  12. package/app/api/workspace/[id]/smith/route.ts +22 -0
  13. package/app/api/workspace/[id]/stream/route.ts +28 -0
  14. package/app/api/workspace/route.ts +100 -0
  15. package/app/global-error.tsx +10 -4
  16. package/app/icon.ico +0 -0
  17. package/app/layout.tsx +2 -2
  18. package/app/login/LoginForm.tsx +96 -0
  19. package/app/login/page.tsx +7 -98
  20. package/app/page.tsx +2 -2
  21. package/bin/forge-server.mjs +13 -1
  22. package/check-forge-status.sh +9 -0
  23. package/components/ConversationEditor.tsx +411 -0
  24. package/components/ConversationGraphView.tsx +347 -0
  25. package/components/ConversationTerminalView.tsx +303 -0
  26. package/components/Dashboard.tsx +36 -39
  27. package/components/DashboardWrapper.tsx +9 -0
  28. package/components/DeliveryFlowEditor.tsx +491 -0
  29. package/components/DeliveryList.tsx +230 -0
  30. package/components/DeliveryWorkspace.tsx +589 -0
  31. package/components/DocTerminal.tsx +10 -2
  32. package/components/DocsViewer.tsx +10 -2
  33. package/components/HelpTerminal.tsx +11 -6
  34. package/components/InlinePipelineView.tsx +111 -0
  35. package/components/MobileView.tsx +20 -0
  36. package/components/MonitorPanel.tsx +9 -4
  37. package/components/NewTaskModal.tsx +32 -0
  38. package/components/PipelineEditor.tsx +49 -6
  39. package/components/PipelineView.tsx +482 -64
  40. package/components/ProjectDetail.tsx +314 -56
  41. package/components/ProjectManager.tsx +49 -4
  42. package/components/SessionView.tsx +27 -13
  43. package/components/SettingsModal.tsx +790 -124
  44. package/components/SkillsPanel.tsx +31 -8
  45. package/components/TaskBoard.tsx +3 -0
  46. package/components/WebTerminal.tsx +257 -43
  47. package/components/WorkspaceTree.tsx +221 -0
  48. package/components/WorkspaceView.tsx +2245 -0
  49. package/install.sh +2 -2
  50. package/lib/agents/claude-adapter.ts +104 -0
  51. package/lib/agents/generic-adapter.ts +64 -0
  52. package/lib/agents/index.ts +242 -0
  53. package/lib/agents/types.ts +70 -0
  54. package/lib/artifacts.ts +106 -0
  55. package/lib/delivery.ts +787 -0
  56. package/lib/forge-skills/forge-inbox.md +37 -0
  57. package/lib/forge-skills/forge-send.md +40 -0
  58. package/lib/forge-skills/forge-status.md +32 -0
  59. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  60. package/lib/help-docs/00-overview.md +7 -1
  61. package/lib/help-docs/01-settings.md +159 -2
  62. package/lib/help-docs/05-pipelines.md +89 -0
  63. package/lib/help-docs/07-projects.md +35 -1
  64. package/lib/help-docs/11-workspace.md +254 -0
  65. package/lib/help-docs/CLAUDE.md +7 -2
  66. package/lib/init.ts +60 -10
  67. package/lib/pipeline.ts +537 -1
  68. package/lib/settings.ts +115 -22
  69. package/lib/skills.ts +249 -372
  70. package/lib/task-manager.ts +113 -33
  71. package/lib/telegram-bot.ts +33 -1
  72. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  73. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  74. package/lib/workspace/agent-bus.ts +416 -0
  75. package/lib/workspace/agent-worker.ts +667 -0
  76. package/lib/workspace/backends/api-backend.ts +262 -0
  77. package/lib/workspace/backends/cli-backend.ts +479 -0
  78. package/lib/workspace/index.ts +82 -0
  79. package/lib/workspace/manager.ts +136 -0
  80. package/lib/workspace/orchestrator.ts +1914 -0
  81. package/lib/workspace/persistence.ts +310 -0
  82. package/lib/workspace/presets.ts +170 -0
  83. package/lib/workspace/skill-installer.ts +188 -0
  84. package/lib/workspace/smith-memory.ts +498 -0
  85. package/lib/workspace/types.ts +231 -0
  86. package/lib/workspace/watch-manager.ts +288 -0
  87. package/lib/workspace-standalone.ts +814 -0
  88. package/middleware.ts +1 -0
  89. package/next-env.d.ts +1 -1
  90. package/package.json +4 -1
  91. package/src/config/index.ts +12 -1
  92. package/src/core/db/database.ts +1 -0
  93. package/start.sh +7 -0
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="app/icon.svg" width="80" height="80" alt="Forge">
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
- | **Vibe Coding** | Browser tmux terminal, multi-tab, split panes, persistent sessions |
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.4.16
1
+ # Forge v0.5.1
2
2
 
3
- Released: 2026-03-23
3
+ Released: 2026-03-28
4
4
 
5
- ## Changes since v0.4.15
5
+ ## Changes since v0.5.0
6
6
 
7
7
  ### Features
8
- - feat: change default port from 3000/3001 to 8403/8404
9
- - feat: prompt to restart Claude after skill/command installation
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: only show port change warning when using default port
15
- - fix: upgrade Next.js 16.1.6 16.2.1 to fix Turbopack panic
16
- - fix: start.sh exports PORT=8403 and TERMINAL_PORT=8404
17
- - fix: default config port 3000 8403
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: update README port 3000 8403
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
- - Revert "feat: mobile real-time streaming with tool activity display"
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.4.15...v0.4.16
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 settings = loadSettings();
21
- const claudePath = settings.claudePath || 'claude';
21
+ const { getAgent } = require('@/lib/agents');
22
+ const adapter = getAgent(agentId);
22
23
  const projectName = projectPath.split('/').pop() || projectPath;
23
24
 
24
- const args = ['-p', '--dangerously-skip-permissions', '--output-format', 'json'];
25
- if (resume) args.push('-c');
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(claudePath, args, {
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
 
@@ -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
- return { count: parseInt(count), pid };
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
- tunnel: { running: tunnel.count > 0, pid: tunnel.pid, url: tunnelUrl },
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 { action } = await req.json();
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
  }
@@ -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
+ }
@@ -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.message}</p>
9
- <button onClick={reset} style={{ marginTop: '1rem', padding: '0.5rem 1rem', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
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>