@aion0/forge 0.4.15 → 0.5.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 (100) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/RELEASE_NOTES.md +170 -13
  4. package/app/api/agents/route.ts +17 -0
  5. package/app/api/delivery/[id]/route.ts +62 -0
  6. package/app/api/delivery/route.ts +40 -0
  7. package/app/api/mobile-chat/route.ts +13 -7
  8. package/app/api/monitor/route.ts +10 -6
  9. package/app/api/pipelines/[id]/route.ts +16 -3
  10. package/app/api/tasks/route.ts +2 -1
  11. package/app/api/workspace/[id]/agents/route.ts +35 -0
  12. package/app/api/workspace/[id]/memory/route.ts +23 -0
  13. package/app/api/workspace/[id]/smith/route.ts +22 -0
  14. package/app/api/workspace/[id]/stream/route.ts +28 -0
  15. package/app/api/workspace/route.ts +100 -0
  16. package/app/global-error.tsx +10 -4
  17. package/app/icon.ico +0 -0
  18. package/app/layout.tsx +2 -2
  19. package/app/login/LoginForm.tsx +96 -0
  20. package/app/login/page.tsx +7 -98
  21. package/app/page.tsx +2 -2
  22. package/bin/forge-server.mjs +23 -4
  23. package/check-forge-status.sh +9 -0
  24. package/cli/mw.ts +2 -2
  25. package/components/ConversationEditor.tsx +411 -0
  26. package/components/ConversationGraphView.tsx +347 -0
  27. package/components/ConversationTerminalView.tsx +303 -0
  28. package/components/Dashboard.tsx +36 -39
  29. package/components/DashboardWrapper.tsx +9 -0
  30. package/components/DeliveryFlowEditor.tsx +491 -0
  31. package/components/DeliveryList.tsx +230 -0
  32. package/components/DeliveryWorkspace.tsx +589 -0
  33. package/components/DocTerminal.tsx +12 -4
  34. package/components/DocsViewer.tsx +10 -2
  35. package/components/HelpTerminal.tsx +13 -8
  36. package/components/InlinePipelineView.tsx +111 -0
  37. package/components/MobileView.tsx +20 -0
  38. package/components/MonitorPanel.tsx +9 -4
  39. package/components/NewTaskModal.tsx +32 -0
  40. package/components/PipelineEditor.tsx +49 -6
  41. package/components/PipelineView.tsx +482 -64
  42. package/components/ProjectDetail.tsx +314 -56
  43. package/components/ProjectManager.tsx +49 -4
  44. package/components/SessionView.tsx +27 -13
  45. package/components/SettingsModal.tsx +790 -124
  46. package/components/SkillsPanel.tsx +34 -8
  47. package/components/TaskBoard.tsx +3 -0
  48. package/components/WebTerminal.tsx +259 -45
  49. package/components/WorkspaceTree.tsx +221 -0
  50. package/components/WorkspaceView.tsx +2224 -0
  51. package/docs/LOCAL-DEPLOY.md +15 -15
  52. package/install.sh +2 -2
  53. package/lib/agents/claude-adapter.ts +104 -0
  54. package/lib/agents/generic-adapter.ts +64 -0
  55. package/lib/agents/index.ts +242 -0
  56. package/lib/agents/types.ts +70 -0
  57. package/lib/artifacts.ts +106 -0
  58. package/lib/cloudflared.ts +1 -1
  59. package/lib/delivery.ts +787 -0
  60. package/lib/forge-skills/forge-inbox.md +37 -0
  61. package/lib/forge-skills/forge-send.md +40 -0
  62. package/lib/forge-skills/forge-status.md +32 -0
  63. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  64. package/lib/help-docs/00-overview.md +8 -2
  65. package/lib/help-docs/01-settings.md +159 -2
  66. package/lib/help-docs/05-pipelines.md +95 -6
  67. package/lib/help-docs/07-projects.md +35 -1
  68. package/lib/help-docs/11-workspace.md +204 -0
  69. package/lib/help-docs/CLAUDE.md +5 -2
  70. package/lib/init.ts +62 -12
  71. package/lib/pipeline.ts +537 -1
  72. package/lib/settings.ts +115 -22
  73. package/lib/skills.ts +249 -372
  74. package/lib/task-manager.ts +113 -33
  75. package/lib/telegram-bot.ts +33 -1
  76. package/lib/telegram-standalone.ts +1 -1
  77. package/lib/terminal-server.ts +2 -2
  78. package/lib/terminal-standalone.ts +1 -1
  79. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  80. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  81. package/lib/workspace/agent-bus.ts +416 -0
  82. package/lib/workspace/agent-worker.ts +667 -0
  83. package/lib/workspace/backends/api-backend.ts +262 -0
  84. package/lib/workspace/backends/cli-backend.ts +479 -0
  85. package/lib/workspace/index.ts +82 -0
  86. package/lib/workspace/manager.ts +136 -0
  87. package/lib/workspace/orchestrator.ts +1804 -0
  88. package/lib/workspace/persistence.ts +310 -0
  89. package/lib/workspace/presets.ts +170 -0
  90. package/lib/workspace/skill-installer.ts +188 -0
  91. package/lib/workspace/smith-memory.ts +498 -0
  92. package/lib/workspace/types.ts +231 -0
  93. package/lib/workspace/watch-manager.ts +288 -0
  94. package/lib/workspace-standalone.ts +790 -0
  95. package/middleware.ts +1 -0
  96. package/next-env.d.ts +1 -1
  97. package/package.json +5 -2
  98. package/src/config/index.ts +13 -2
  99. package/src/core/db/database.ts +1 -0
  100. package/start.sh +10 -0
package/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ forge server start # production via npm link/install
9
9
  forge server start --dev # dev mode
10
10
  forge server start # background by default, logs to ~/.forge/forge.log
11
11
  forge server start --foreground # foreground mode
12
- forge server stop # stop default instance (port 3000)
12
+ forge server stop # stop default instance (port 8403)
13
13
  forge server stop --port 4000 --dir ~/.forge-staging # stop specific instance
14
14
  forge server restart # stop + start (safe for remote)
15
15
  forge server rebuild # force rebuild
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>
@@ -32,7 +32,7 @@ npm install -g @aion0/forge
32
32
  forge server start
33
33
  ```
34
34
 
35
- Open `http://localhost:3000`. First launch prompts you to set an admin password.
35
+ Open `http://localhost:8403`. First launch prompts you to set an admin password.
36
36
 
37
37
  **Requirements:** Node.js ≥ 20, tmux, [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
38
38
 
package/RELEASE_NOTES.md CHANGED
@@ -1,25 +1,182 @@
1
- # Forge v0.4.15
1
+ # Forge v0.5.0
2
2
 
3
- Released: 2026-03-23
3
+ Released: 2026-03-28
4
4
 
5
- ## Changes since v0.4.14
5
+ ## Changes since v0.4.16
6
6
 
7
7
  ### Features
8
- - feat: record token usage from task, pipeline, and mobile sources
9
- - feat: Usage dashboardtoken cost by project, model, day, source
10
- - feat: token usage tracking scanner, DB, API
8
+ - feat: watch actions (log/analyze/approve) + config UI
9
+ - feat: agent watchautonomous periodic monitoring
10
+ - feat: Workspace multi-agent terminal view per project
11
+ - feat: requires-driven scheduling for delivery engine
12
+ - feat: standardized envelope format + request/response audit trail
13
+ - feat: visible data contracts on flow editor — artifact names on edges and nodes
14
+ - feat: ReactFlow-based delivery role editor — drag & connect agent topology
15
+ - feat: customizable delivery roles — users compose agent phases from presets
16
+ - feat: show all 4 agent panels in 2x2 grid with SVG flow arrows
17
+ - feat: Delivery Workspace — multi-agent orchestrated software delivery
18
+ - feat: conversation terminal view with inline input and data flow
19
+ - feat: Conversation Mode — multi-agent dialogue with graph view and live logs
20
+ - feat: Pipeline editor node edit modal has Agent + Mode selectors
21
+ - feat: Pipeline UI shows agent per node
22
+ - feat: per-doc-root agent config (Settings + Docs toolbar)
23
+ - feat: Telegram default agent + Docs agent config
24
+ - feat: TTY support for agents that need terminal (e.g. Codex)
25
+ - feat: per-agent skip permissions flag with presets
26
+ - feat: Telegram agent support — @agent syntax + /agents command
27
+ - feat: agent selection for Pipeline, Mobile, Help
28
+ - feat: per-scene model config for each agent
29
+ - feat: model config moved from global to per-agent
30
+ - feat: task system uses agent adapter + agent selector in NewTaskModal
31
+ - feat: terminal tab agent selection
32
+ - feat: Agents management UI in Settings
33
+ - feat: multi-agent foundation — registry, adapters, API
11
34
 
12
35
  ### Bug Fixes
13
- - fix: exclude cache tokens from cost estimate
14
- - fix: usage stored per day+model for accurate daily breakdown
15
- - fix: usage query uses local timezone for daily grouping
36
+ - fix: tolerate Next.js 16 _global-error prerender bug in build
37
+ - fix: Log panel reads from persistent logs.jsonl + clear logs button
38
+ - fix: restart watch loop when agent config is updated
39
+ - fix: watch directory picker uses correct tree type and flattens nested dirs
40
+ - fix: send block only when message is still running, not after done
41
+ - fix: block forge-send reply to message sender + skill prompt hint
42
+ - fix: hasRunning check uses worker's current message, not all bus log
43
+ - fix: no extra reply message when processing downstream request
44
+ - fix: smith send API returns messageId for outbox tracking
45
+ - fix: inject FORGE_AGENT_ID/WORKSPACE_ID/PORT into manual terminal env
46
+ - fix: forge skills use $FORGE_AGENT_ID instead of hardcoded 'unknown'
47
+ - fix: deduplicate bus_message SSE events by message ID
48
+ - fix: emit done before markMessageDone so causedBy can read messageId
49
+ - fix: remove notifyDownstreamForRevalidation + prevent multiple running messages
50
+ - fix: abort_message no longer errors on already-aborted messages
51
+ - fix: getAllAgentStates returns worker state with entry mode override
52
+ - fix: manual stays in mode field, displayed as purple 'manual' on node
53
+ - fix: show 'manual' task status when agent is in manual mode
54
+ - fix: close terminal uses close_terminal action instead of reset
55
+ - fix: restartAgentDaemon recreates worker after resetAgent kills it
56
+ - fix: parseBusMarkers re-scanning entire history causes message loops
57
+ - fix: restartAgentDaemon aligned with simplified setManualMode
58
+ - fix: manual mode shows down because worker async cleanup overrides state
59
+ - fix: buffered wake prevents lost messages in daemon loop
60
+ - fix: simplify retry — reset original message to pending, no emit
61
+ - fix: retry creates new message, preserves original for history
62
+ - fix: message retry causing duplicate execution
63
+ - fix: PTY spawn in ESM — use createRequire for node-pty
64
+ - fix: TTY detection for codex profiles + clear agent cache on terminal open
65
+ - fix: project terminal dialog loads sessions from claude-sessions API
66
+ - fix: bypass GitHub CDN cache on skills sync
67
+ - fix: merge tags from info.json during v2 registry sync
68
+ - fix: enable allowProposedApi for search decorations
69
+ - fix: profile env/model propagation across all terminal launch paths
70
+ - fix: saveAgentConfig preserves profile fields (base/env/model/type)
71
+ - fix: sessions API uses orch.projectPath, ESM imports, non-claude compat
72
+ - fix: stricter workDir validation — block .. and sibling dir escape
73
+ - fix: workDir with leading / treated as relative to project, not absolute
74
+ - fix: workDir normalize strips ./ prefix, default to smith label
75
+ - fix: only use claude -c (resume) if existing session exists
76
+ - fix: handle unknown agentId in smith send API
77
+ - fix: whitelist /api/workspace in middleware for forge skill auto-discover
78
+ - fix: install skills as directories with SKILL.md (Claude Code format)
79
+ - fix: use imported resolve instead of require('node:path') in ESM context
80
+ - fix: orchestrator actively manages smith lifecycle in start/stop daemon
81
+ - fix: startDaemon error handling + stopDaemon cleanup
82
+ - fix: close terminal should enter listening, not execute steps
83
+ - fix: workspace terminal uses correct message types from terminal server
84
+ - fix: workspace terminal input + keep alive when switching tabs
85
+ - fix: rewrite WorkspaceView — each agent is a real interactive terminal
86
+ - fix: move ssr:false dynamic import to client wrapper component
87
+ - fix: disable SSR for Dashboard to eliminate hydration mismatch
88
+ - fix: default phases missing _outputArtifactName/_label/_icon metadata
89
+ - fix: data flow arrows based on requires/produces, not sequential order
90
+ - fix: suppress hydration warning from locale/extension mismatch
91
+ - fix: pipeline node shows 'default' instead of 'claude' when no agent set
92
+ - fix: PTY onExit/onData registered once, fixes stuck tasks after cancel
93
+ - fix: auto-kill PTY agents after 15s idle
94
+ - fix: strip ANSI/terminal control codes from PTY agent output
95
+ - fix: retryTask preserves original agent selection
96
+ - fix: use pipe stdin for task spawn, close immediately after
97
+ - fix: non-claude agents no longer fallback to claude or show claude model
98
+ - fix: settings agent config debounced save + unsaved warning on close
99
+ - fix: generic agents use taskFlags from settings, log raw text output
100
+ - fix: only pass model flag to agents that support it, show agent in task
101
+ - fix: settings agent colors match terminal — use API detected status
102
+ - fix: show all configured agents, not just detected ones
16
103
 
17
104
  ### Performance
18
- - perf: usage scan interval from 5min to 1 hour
105
+ - perf: watch heartbeat only logs to console, not to files/history
106
+ - perf: watch uses timestamp comparison instead of full snapshot
107
+
108
+ ### Refactoring
109
+ - refactor: remove Delivery tab, keep only Workspace
110
+
111
+ ### Documentation
112
+ - docs: update workspace help with message system design
113
+ - docs: add workspace help, update settings/projects/overview docs
19
114
 
20
115
  ### Other
21
- - ui: show author and source URL in skills detail view
22
- - ui: move Usage button next to Browser in header right section
116
+ - watch: push heartbeat + alert logs to agent history for Log panel
117
+ - watch: log heartbeat on each check cycle
118
+ - ui: structured watch target builder with directory picker
119
+ - debug: log deleteMessage results for diagnosing message reappearance
120
+ - persist currentMessageId as task trigger identifier
121
+ - inbox: abort all pending button for batch operations
122
+ - inbox/outbox: batch select + delete for completed messages
123
+ - Phase 3: ticket UI, retry limits, ticket API actions
124
+ - Phase 2: causedBy chain + ticket messages + receive rules
125
+ - Phase 1: anti-loop — DAG cycle detection, directional broadcast, disable SEND markers
126
+ - ui: show mode (auto/manual) as separate line on agent node
127
+ - simplify: setManualMode only changes mode, message loop skips manual
128
+ - debug: log when agent message loop skips due to not-listening state
129
+ - skills: auto-loop sync with progress indicator
130
+ - skills: incremental sync — registry fast, info.json in batches
131
+ - terminal: add Ctrl/Cmd+F search in terminal buffer
132
+ - terminal: add WebGL rendering + Unicode 11 support
133
+ - unified terminal launch: resolveTerminalLaunch for both VibeCoding + Workspace
134
+ - settings: agent has profile selector dropdown
135
+ - settings: profiles are global/shared, not per-agent
136
+ - settings: add cliType selector to agent config panel
137
+ - settings: profiles nested inside each agent, not standalone section
138
+ - open_terminal: return cliType + cliCmd from agent registry
139
+ - settings: env var templates per CLI type for profiles
140
+ - agent config: add cliType field (claude-code/codex/aider/generic)
141
+ - vibecoding: profile selector + session picker + env injection for terminal
142
+ - terminal profile: env var injection via export, not settings.json
143
+ - terminal: session picker with recent sessions list
144
+ - terminal: styled launch dialog — New Session / Resume Latest
145
+ - terminal: simple prompt dialog for new/resume before opening
146
+ - terminal: prompt user to choose new session or resume
147
+ - cleanup: simplify resume flag check in FloatingTerminal
148
+ - UI: show resolved workDir path hint below input
149
+ - forge skills: explicit 2-step commands, no env var checks
150
+ - forge skills: inline auto-discover, no separate setup step
151
+ - workDir validation: unique per smith, must be within project, no nesting
152
+ - profile settings: write to smith workDir, not project root
153
+ - profile terminal: apply env/model to .claude/settings.json on open_terminal
154
+ - forge skills: install/update on every forge startup
155
+ - forge skills: auto-discover workspace context with fallback defaults
156
+ - skills: install once in startDaemon, remove per-smith install
157
+ - skills: install to ~/.claude/skills/ globally + fix project deny rules
158
+ - skill installer: auto-fix .claude/settings.json curl permissions
159
+ - forge skills: env var injection + auto-install on smith startup
160
+ - agent profiles: editable profile rows with expand/collapse
161
+ - agent profiles: env vars support for custom CLI configs (e.g., FortiAI)
162
+ - agent profiles UI: settings management + workspace profile selector
163
+ - agent profiles + provider config data layer
164
+ - smith message-driven architecture: independent message loop, inbox management, status simplification
165
+ - multiple agent implementation
166
+ - fix issue
167
+ - fix issue for workspace
168
+ - optmized projects
169
+ - refactoring workspace
170
+ - implement workspace and fix issue
171
+ - implement multiple agents
172
+ - ui: show agent badge on ReactFlow node blocks in pipeline editor
173
+ - simplify: single docs agent instead of per-root
174
+ - ui: remove Docs page agent selector, keep Settings-only config
175
+ - ui: remove leftover model/permissions migration notes from Settings
176
+ - ui: consistent agent colors across terminal and settings
177
+ - ui: agent buttons green=installed, gray=not installed
178
+ - ui: agent buttons ≤3 inline, >3 overflow with dropdown
179
+ - ui: agent buttons inline with project name in new tab modal
23
180
 
24
181
 
25
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.4.14...v0.4.15
182
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.4.16...v0.5.0
@@ -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
+ }