@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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Backend — executes agent steps via Vercel AI SDK (generateText + tools).
|
|
3
|
+
*
|
|
4
|
+
* Uses the subscription-free API path: requires an API key.
|
|
5
|
+
* Provides full tool control and maxSteps auto tool loop.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { generateText, stepCountIs, type ModelMessage } from 'ai';
|
|
9
|
+
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
11
|
+
import { resolve, dirname } from 'node:path';
|
|
12
|
+
import { getModel } from '@/src/core/providers/registry';
|
|
13
|
+
import type { AgentBackend, StepExecutionParams, StepExecutionResult, Artifact } from '../types';
|
|
14
|
+
import type { TaskLogEntry } from '@/src/types';
|
|
15
|
+
|
|
16
|
+
// ─── Tool factory ────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function createTools(projectPath: string, artifacts: Artifact[], onLog?: (e: TaskLogEntry) => void) {
|
|
19
|
+
const ts = () => new Date().toISOString();
|
|
20
|
+
|
|
21
|
+
const safePath = (p: string) => {
|
|
22
|
+
const abs = resolve(projectPath, p);
|
|
23
|
+
const root = resolve(projectPath) + '/';
|
|
24
|
+
// Must be exactly the project root or a child (trailing slash prevents /project-evil matching /project)
|
|
25
|
+
if (abs !== resolve(projectPath) && !abs.startsWith(root)) throw new Error(`Path outside project: ${p}`);
|
|
26
|
+
return abs;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
read_file: {
|
|
31
|
+
description: 'Read the contents of a file. Path is relative to project root.',
|
|
32
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
33
|
+
execute: async ({ path }: { path: string }) => {
|
|
34
|
+
const abs = safePath(path);
|
|
35
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: path, tool: 'read_file', timestamp: ts() });
|
|
36
|
+
try {
|
|
37
|
+
const content = readFileSync(abs, 'utf-8');
|
|
38
|
+
onLog?.({ type: 'assistant', subtype: 'tool_result', content: `Read ${content.length} chars`, timestamp: ts() });
|
|
39
|
+
return content;
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
return `Error reading file: ${err.message}`;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
write_file: {
|
|
47
|
+
description: 'Write content to a file. Creates parent directories if needed.',
|
|
48
|
+
parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
|
|
49
|
+
execute: async ({ path, content }: { path: string; content: string }) => {
|
|
50
|
+
const abs = safePath(path);
|
|
51
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: `write ${path} (${content.length} chars)`, tool: 'write_file', timestamp: ts() });
|
|
52
|
+
try {
|
|
53
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
54
|
+
writeFileSync(abs, content, 'utf-8');
|
|
55
|
+
artifacts.push({ type: 'file', path, summary: `Written ${content.length} chars` });
|
|
56
|
+
onLog?.({ type: 'assistant', subtype: 'tool_result', content: `Wrote ${path}`, timestamp: ts() });
|
|
57
|
+
return `Successfully wrote ${path}`;
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
return `Error writing file: ${err.message}`;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
list_dir: {
|
|
65
|
+
description: 'List files and directories. Path is relative to project root.',
|
|
66
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: [] },
|
|
67
|
+
execute: async ({ path = '.' }: { path?: string }) => {
|
|
68
|
+
const abs = safePath(path || '.');
|
|
69
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: path || '.', tool: 'list_dir', timestamp: ts() });
|
|
70
|
+
try {
|
|
71
|
+
const entries = readdirSync(abs, { withFileTypes: true });
|
|
72
|
+
return entries.map(e => `${e.isDirectory() ? '[dir]' : '[file]'} ${e.name}`).join('\n');
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
return `Error listing directory: ${err.message}`;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
search_files: {
|
|
80
|
+
description: 'Search for text content in files using grep.',
|
|
81
|
+
parameters: { type: 'object', properties: { pattern: { type: 'string' }, path: { type: 'string' } }, required: ['pattern'] },
|
|
82
|
+
execute: async ({ pattern, path = '.' }: { pattern: string; path?: string }) => {
|
|
83
|
+
const abs = safePath(path || '.');
|
|
84
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: `grep "${pattern}" in ${path || '.'}`, tool: 'search_files', timestamp: ts() });
|
|
85
|
+
try {
|
|
86
|
+
// Use execFileSync to avoid shell injection — pattern is passed as argument, not interpolated
|
|
87
|
+
const result = execFileSync('grep', ['-rn', '--include=*', pattern, abs], {
|
|
88
|
+
encoding: 'utf-8', timeout: 10000, maxBuffer: 512 * 1024,
|
|
89
|
+
}).split('\n').slice(0, 50).join('\n');
|
|
90
|
+
return result || 'No matches found';
|
|
91
|
+
} catch {
|
|
92
|
+
return 'No matches found';
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
run_command: {
|
|
98
|
+
description: 'Run a shell command in the project directory.',
|
|
99
|
+
parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
|
|
100
|
+
execute: async ({ command }: { command: string }) => {
|
|
101
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: command, tool: 'run_command', timestamp: ts() });
|
|
102
|
+
try {
|
|
103
|
+
const result = execSync(command, { cwd: projectPath, encoding: 'utf-8', timeout: 60000, maxBuffer: 1024 * 1024 });
|
|
104
|
+
const truncated = result.length > 5000 ? result.slice(0, 5000) + '\n... (truncated)' : result;
|
|
105
|
+
onLog?.({ type: 'assistant', subtype: 'tool_result', content: truncated, timestamp: ts() });
|
|
106
|
+
return truncated;
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
const msg = err.stderr || err.message || String(err);
|
|
109
|
+
onLog?.({ type: 'assistant', subtype: 'tool_result', content: `Error: ${msg}`, timestamp: ts() });
|
|
110
|
+
return `Command failed: ${msg}`;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Create inter-agent communication tools (only if bus callbacks are provided) */
|
|
118
|
+
function createCommTools(
|
|
119
|
+
agentId: string,
|
|
120
|
+
peerAgentIds: string[],
|
|
121
|
+
onBusSend?: (to: string, content: string) => void,
|
|
122
|
+
onBusRequest?: (to: string, question: string) => Promise<string>,
|
|
123
|
+
onLog?: (e: TaskLogEntry) => void,
|
|
124
|
+
) {
|
|
125
|
+
const ts = () => new Date().toISOString();
|
|
126
|
+
const tools: Record<string, any> = {};
|
|
127
|
+
|
|
128
|
+
if (onBusSend) {
|
|
129
|
+
tools.notify_agent = {
|
|
130
|
+
description: `Send a notification message to another agent. Available agents: ${peerAgentIds.join(', ')}`,
|
|
131
|
+
parameters: { type: 'object', properties: {
|
|
132
|
+
to: { type: 'string', description: 'Target agent ID' },
|
|
133
|
+
message: { type: 'string', description: 'Message content' },
|
|
134
|
+
}, required: ['to', 'message'] },
|
|
135
|
+
execute: async ({ to, message }: { to: string; message: string }) => {
|
|
136
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: `→ ${to}: ${message}`, tool: 'notify_agent', timestamp: ts() });
|
|
137
|
+
onBusSend(to, message);
|
|
138
|
+
return `Message sent to ${to}`;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (onBusRequest) {
|
|
144
|
+
tools.ask_agent = {
|
|
145
|
+
description: `Ask another agent a question and wait for their response. Available agents: ${peerAgentIds.join(', ')}`,
|
|
146
|
+
parameters: { type: 'object', properties: {
|
|
147
|
+
to: { type: 'string', description: 'Target agent ID' },
|
|
148
|
+
question: { type: 'string', description: 'Question to ask' },
|
|
149
|
+
}, required: ['to', 'question'] },
|
|
150
|
+
execute: async ({ to, question }: { to: string; question: string }) => {
|
|
151
|
+
onLog?.({ type: 'assistant', subtype: 'tool_use', content: `→ ${to}: ${question}`, tool: 'ask_agent', timestamp: ts() });
|
|
152
|
+
try {
|
|
153
|
+
const response = await onBusRequest(to, question);
|
|
154
|
+
onLog?.({ type: 'assistant', subtype: 'tool_result', content: `← ${to}: ${response}`, timestamp: ts() });
|
|
155
|
+
return response;
|
|
156
|
+
} catch (err: any) {
|
|
157
|
+
return `No response from ${to}: ${err.message}`;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return tools;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── History → AI SDK messages ───────────────────────────
|
|
167
|
+
|
|
168
|
+
function historyToMessages(history: TaskLogEntry[]): ModelMessage[] {
|
|
169
|
+
const messages: ModelMessage[] = [];
|
|
170
|
+
// Only include last 3 step results + truncate tool results to save tokens
|
|
171
|
+
const MAX_HISTORY_STEPS = 3;
|
|
172
|
+
const MAX_TOOL_RESULT = 500;
|
|
173
|
+
|
|
174
|
+
// Filter to step-level results only (skip individual tool calls)
|
|
175
|
+
const stepResults = history
|
|
176
|
+
.filter(m => m.type === 'result' && m.subtype === 'step_complete')
|
|
177
|
+
.slice(-MAX_HISTORY_STEPS);
|
|
178
|
+
|
|
179
|
+
let currentAssistant = '';
|
|
180
|
+
|
|
181
|
+
for (const entry of stepResults) {
|
|
182
|
+
const truncated = entry.content.length > 1000
|
|
183
|
+
? entry.content.slice(0, 1000) + '... (truncated)'
|
|
184
|
+
: entry.content;
|
|
185
|
+
currentAssistant += (currentAssistant ? '\n\n' : '') + truncated;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (currentAssistant) {
|
|
189
|
+
messages.push({ role: 'assistant', content: currentAssistant });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return messages;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── API Backend class ───────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export class ApiBackend implements AgentBackend {
|
|
198
|
+
private abortController: AbortController | null = null;
|
|
199
|
+
|
|
200
|
+
async executeStep(params: StepExecutionParams): Promise<StepExecutionResult> {
|
|
201
|
+
const { config, step, history, projectPath, upstreamContext, onLog,
|
|
202
|
+
onBusSend, onBusRequest, peerAgentIds } = params;
|
|
203
|
+
|
|
204
|
+
if (!config.provider) throw new Error('API backend requires a provider');
|
|
205
|
+
|
|
206
|
+
this.abortController = new AbortController();
|
|
207
|
+
const model = getModel(config.provider, config.model);
|
|
208
|
+
const artifacts: Artifact[] = [];
|
|
209
|
+
|
|
210
|
+
// Build messages: history context + current step prompt
|
|
211
|
+
const messages: ModelMessage[] = historyToMessages(history);
|
|
212
|
+
|
|
213
|
+
let userPrompt = step.prompt;
|
|
214
|
+
if (upstreamContext) {
|
|
215
|
+
userPrompt = `## Upstream agent output:\n${upstreamContext}\n\n---\n\n${userPrompt}`;
|
|
216
|
+
}
|
|
217
|
+
messages.push({ role: 'user', content: userPrompt });
|
|
218
|
+
|
|
219
|
+
// Create tools: filesystem + communication
|
|
220
|
+
const fsTools = createTools(projectPath, artifacts, onLog);
|
|
221
|
+
const commTools = createCommTools(config.id, peerAgentIds || [], onBusSend, onBusRequest, onLog);
|
|
222
|
+
const tools = { ...fsTools, ...commTools } as any;
|
|
223
|
+
|
|
224
|
+
onLog?.({
|
|
225
|
+
type: 'system',
|
|
226
|
+
subtype: 'init',
|
|
227
|
+
content: `Step "${step.label}" — ${config.provider}/${config.model || 'default'}`,
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const result = await generateText({
|
|
232
|
+
model,
|
|
233
|
+
system: config.role,
|
|
234
|
+
messages,
|
|
235
|
+
tools,
|
|
236
|
+
stopWhen: stepCountIs(20),
|
|
237
|
+
abortSignal: this.abortController.signal,
|
|
238
|
+
onStepFinish: ({ text }: { text?: string }) => {
|
|
239
|
+
if (text) {
|
|
240
|
+
onLog?.({
|
|
241
|
+
type: 'assistant',
|
|
242
|
+
subtype: 'text',
|
|
243
|
+
content: text,
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
} as any);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
response: result.text,
|
|
252
|
+
artifacts,
|
|
253
|
+
inputTokens: result.usage?.inputTokens ?? 0,
|
|
254
|
+
outputTokens: result.usage?.outputTokens ?? 0,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
abort(): void {
|
|
259
|
+
this.abortController?.abort();
|
|
260
|
+
this.abortController = null;
|
|
261
|
+
}
|
|
262
|
+
}
|