@aion0/forge 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.forge/mcp.json +8 -0
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
- package/CLAUDE.md +2 -2
- package/RELEASE_NOTES.md +101 -5
- package/app/api/auth/check/route.ts +18 -0
- package/app/api/browser-bridge/route.ts +70 -0
- package/app/api/chat/sessions/[id]/events/route.ts +17 -0
- package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
- package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
- package/app/api/chat/sessions/[id]/route.ts +23 -0
- package/app/api/chat/sessions/route.ts +12 -0
- package/app/api/chat/temper-ping/route.ts +18 -0
- package/app/api/chat-proxy/[...path]/route.ts +83 -0
- package/app/api/connector-tool/route.ts +38 -0
- package/app/api/connectors/[id]/settings/route.ts +112 -0
- package/app/api/connectors/route.ts +108 -0
- package/app/api/health/tools/route.ts +14 -0
- package/app/api/issue-scanner-gitlab/route.ts +95 -0
- package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
- package/app/api/jobs/[id]/route.ts +31 -0
- package/app/api/jobs/[id]/run/route.ts +44 -0
- package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
- package/app/api/jobs/[id]/runs/route.ts +15 -0
- package/app/api/jobs/preview/route.ts +193 -0
- package/app/api/jobs/route.ts +36 -0
- package/app/api/notify/test/route.ts +39 -7
- package/app/api/pipelines/[id]/route.ts +10 -1
- package/app/api/pipelines/route.ts +16 -2
- package/app/api/plugins/route.ts +40 -8
- package/app/api/project-sessions/route.ts +50 -10
- package/app/api/settings/route.ts +13 -0
- package/app/chat/page.tsx +531 -0
- package/bin/forge-server.mjs +3 -1
- package/cli/chat.ts +283 -0
- package/cli/jobs.ts +176 -0
- package/cli/mw.ts +28 -1
- package/cli/worktree.ts +245 -0
- package/components/ConnectorsPanel.tsx +275 -0
- package/components/Dashboard.tsx +90 -37
- package/components/JobsView.tsx +361 -0
- package/components/LogViewer.tsx +12 -2
- package/components/PipelineView.tsx +275 -56
- package/components/PluginsPanel.tsx +3 -1
- package/components/SettingsModal.tsx +229 -40
- package/components/SkillsPanel.tsx +12 -4
- package/components/TerminalLauncher.tsx +3 -1
- package/components/WebTerminal.tsx +32 -9
- package/components/WorkspaceView.tsx +18 -10
- package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
- package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
- package/docs/Implementation-Plan-Browser-Agent.md +487 -0
- package/docs/Jobs-Design.md +240 -0
- package/docs/LOCAL-DEPLOY.md +3 -3
- package/docs/RFC-Browser-Connectors.md +509 -0
- package/lib/agents/index.ts +44 -6
- package/lib/agents/types.ts +1 -1
- package/lib/browser-bridge-standalone.ts +317 -0
- package/lib/builtin-plugins/github-api.yaml +93 -0
- package/lib/builtin-plugins/gitlab.yaml +860 -0
- package/lib/builtin-plugins/mantis.probe.js +176 -0
- package/lib/builtin-plugins/mantis.yaml +964 -0
- package/lib/builtin-plugins/pmdb.yaml +178 -0
- package/lib/builtin-plugins/teams.yaml +913 -0
- package/lib/chat/__test__/smoke.ts +30 -0
- package/lib/chat/agent-loop.ts +523 -0
- package/lib/chat/bridge-client.ts +59 -0
- package/lib/chat/llm/anthropic.ts +99 -0
- package/lib/chat/llm/index.ts +20 -0
- package/lib/chat/llm/openai.ts +215 -0
- package/lib/chat/llm/types.ts +42 -0
- package/lib/chat/local-memory.ts +300 -0
- package/lib/chat/memory-store.ts +87 -0
- package/lib/chat/memory-tools.ts +157 -0
- package/lib/chat/protocols/http.ts +118 -0
- package/lib/chat/protocols/shell.ts +101 -0
- package/lib/chat/proxy.ts +51 -0
- package/lib/chat/session-store.ts +272 -0
- package/lib/chat/telegram-bridge.ts +276 -0
- package/lib/chat/temper.ts +281 -0
- package/lib/chat/tool-dispatcher.ts +190 -0
- package/lib/chat/types.ts +50 -0
- package/lib/chat-standalone.ts +286 -0
- package/lib/crypto.ts +1 -1
- package/lib/health.ts +131 -0
- package/lib/help-docs/00-overview.md +2 -1
- package/lib/help-docs/01-settings.md +46 -25
- package/lib/help-docs/07-projects.md +1 -1
- package/lib/help-docs/10-troubleshooting.md +10 -2
- package/lib/help-docs/16-gitlab-autofix.md +114 -0
- package/lib/help-docs/17-connectors.md +322 -0
- package/lib/help-docs/18-chrome-mcp.md +134 -0
- package/lib/help-docs/19-jobs.md +140 -0
- package/lib/help-docs/20-mantis-bug-fix.md +115 -0
- package/lib/help-docs/CLAUDE.md +10 -0
- package/lib/init.ts +137 -50
- package/lib/iso-time.ts +30 -0
- package/lib/issue-scanner-gitlab.ts +281 -0
- package/lib/jobs/dispatcher.ts +217 -0
- package/lib/jobs/scheduler.ts +334 -0
- package/lib/jobs/store.ts +319 -0
- package/lib/jobs/types.ts +117 -0
- package/lib/pipeline-scheduler.ts +1 -6
- package/lib/pipeline.ts +790 -10
- package/lib/plugins/registry.ts +133 -8
- package/lib/plugins/templates.ts +83 -0
- package/lib/plugins/types.ts +140 -1
- package/lib/session-watcher.ts +36 -10
- package/lib/settings.ts +65 -33
- package/lib/skills.ts +3 -1
- package/lib/task-manager.ts +50 -22
- package/lib/telegram-bot.ts +71 -0
- package/lib/terminal-standalone.ts +58 -36
- package/lib/workspace/orchestrator.ts +1 -0
- package/middleware.ts +10 -0
- package/package.json +3 -2
- package/scripts/bench/README.md +1 -1
- package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
- package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
- package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
- package/src/core/db/database.ts +21 -12
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Browser Bridge — WebSocket control plane for Forge ↔ Extension.
|
|
4
|
+
*
|
|
5
|
+
* Runs as an independent process (like terminal/workspace standalones).
|
|
6
|
+
* Exposes:
|
|
7
|
+
* - HTTP on $BRIDGE_PORT/api/* — push events, RPC dispatch, status.
|
|
8
|
+
* - WebSocket on $BRIDGE_PORT/ws — long-lived bidirectional channel
|
|
9
|
+
* used by the extension service worker.
|
|
10
|
+
*
|
|
11
|
+
* Auth model (post-Phase 4):
|
|
12
|
+
* The bridge accepts the user's existing Forge token (X-Forge-Token,
|
|
13
|
+
* issued by /api/auth/verify against the admin password). On every
|
|
14
|
+
* `hello`, the bridge validates the token via /api/auth/check on
|
|
15
|
+
* Next.js. There's no separate pairing code or long-lived bridge
|
|
16
|
+
* token — extensions reuse the credential they already have.
|
|
17
|
+
*
|
|
18
|
+
* Protocol (newline-delimited JSON frames):
|
|
19
|
+
*
|
|
20
|
+
* Extension → Forge:
|
|
21
|
+
* { type: 'hello', forge_token, version? }
|
|
22
|
+
* { type: 'rpc_result', id, ok, value, error? }
|
|
23
|
+
* { type: 'event', name, payload }
|
|
24
|
+
* { type: 'ping' }
|
|
25
|
+
*
|
|
26
|
+
* Forge → Extension:
|
|
27
|
+
* { type: 'hello_ok', ext_id }
|
|
28
|
+
* { type: 'hello_error', message }
|
|
29
|
+
* { type: 'rpc_request', id, method, params }
|
|
30
|
+
* { type: 'push', topic, payload }
|
|
31
|
+
* { type: 'pong' }
|
|
32
|
+
*
|
|
33
|
+
* RPC `method` examples (extension implements these):
|
|
34
|
+
* browser.execute_script { tabId, world, scriptBody, args }
|
|
35
|
+
* browser.list_tabs { url? }
|
|
36
|
+
* browser.navigate { tabId, url }
|
|
37
|
+
* browser.get_tab { tabId }
|
|
38
|
+
*
|
|
39
|
+
* Usage: npx tsx lib/browser-bridge-standalone.ts [--forge-port=8403]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
43
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
44
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
45
|
+
|
|
46
|
+
// ─── Config ───────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const PORT = Number(process.env.BRIDGE_PORT) || 8407;
|
|
49
|
+
const FORGE_PORT = Number(process.env.PORT) || 8403;
|
|
50
|
+
const RPC_TIMEOUT_MS = 60_000;
|
|
51
|
+
const TOKEN_CACHE_TTL_MS = 60_000;
|
|
52
|
+
|
|
53
|
+
// ─── Forge-token validation (with short-lived cache) ──────
|
|
54
|
+
|
|
55
|
+
const tokenCache = new Map<string, number>(); // token → expires_at
|
|
56
|
+
|
|
57
|
+
async function isForgeTokenValid(token: string): Promise<boolean> {
|
|
58
|
+
if (!token) return false;
|
|
59
|
+
const cached = tokenCache.get(token);
|
|
60
|
+
if (cached && cached > Date.now()) return true;
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`http://127.0.0.1:${FORGE_PORT}/api/auth/check`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'x-forge-token': token, 'content-type': 'application/json' },
|
|
65
|
+
body: '{}',
|
|
66
|
+
});
|
|
67
|
+
if (res.ok) {
|
|
68
|
+
tokenCache.set(token, Date.now() + TOKEN_CACHE_TTL_MS);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
tokenCache.delete(token);
|
|
72
|
+
return false;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.warn('[bridge] token check failed:', (e as Error).message);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Synthetic ext_id derived from the token so reconnects keep stable
|
|
80
|
+
// identity (and so the side panel UI can show "connected as <hash>").
|
|
81
|
+
function extIdForToken(token: string): string {
|
|
82
|
+
const h = createHash('sha256').update(token).digest('hex');
|
|
83
|
+
return 'tok-' + h.slice(0, 12);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Connected extensions ─────────────────────────────────
|
|
87
|
+
|
|
88
|
+
interface ExtClient {
|
|
89
|
+
ext_id: string;
|
|
90
|
+
ws: WebSocket;
|
|
91
|
+
connected_at: number;
|
|
92
|
+
name?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const clients = new Map<string, ExtClient>(); // ext_id → client
|
|
96
|
+
|
|
97
|
+
function pickAnyClient(): ExtClient | null {
|
|
98
|
+
let best: ExtClient | null = null;
|
|
99
|
+
for (const c of clients.values()) {
|
|
100
|
+
if (!best || c.connected_at > best.connected_at) best = c;
|
|
101
|
+
}
|
|
102
|
+
return best;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── RPC tracking (Forge → Extension) ─────────────────────
|
|
106
|
+
|
|
107
|
+
interface PendingRpc {
|
|
108
|
+
resolve: (value: unknown) => void;
|
|
109
|
+
reject: (err: Error) => void;
|
|
110
|
+
timer: NodeJS.Timeout;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pendingRpcs = new Map<string, PendingRpc>(); // rpc_id → callbacks
|
|
114
|
+
|
|
115
|
+
function callExtension(method: string, params: unknown): Promise<unknown> {
|
|
116
|
+
const client = pickAnyClient();
|
|
117
|
+
if (!client) {
|
|
118
|
+
return Promise.reject(new Error('No extension connected to the bridge.'));
|
|
119
|
+
}
|
|
120
|
+
const id = randomUUID();
|
|
121
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
pendingRpcs.delete(id);
|
|
124
|
+
reject(new Error(`RPC ${method} timed out after ${RPC_TIMEOUT_MS / 1000}s`));
|
|
125
|
+
}, RPC_TIMEOUT_MS);
|
|
126
|
+
pendingRpcs.set(id, { resolve, reject, timer });
|
|
127
|
+
client.ws.send(JSON.stringify({ type: 'rpc_request', id, method, params }));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── WebSocket handlers ───────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function handleFrame(ws: WebSocket, raw: string): void {
|
|
134
|
+
let frame: any;
|
|
135
|
+
try { frame = JSON.parse(raw); }
|
|
136
|
+
catch {
|
|
137
|
+
ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
switch (frame.type) {
|
|
142
|
+
case 'hello':
|
|
143
|
+
void handleHello(ws, frame);
|
|
144
|
+
break;
|
|
145
|
+
case 'rpc_result':
|
|
146
|
+
handleRpcResult(frame);
|
|
147
|
+
break;
|
|
148
|
+
case 'event':
|
|
149
|
+
handleEvent(frame);
|
|
150
|
+
break;
|
|
151
|
+
case 'ping':
|
|
152
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
ws.send(JSON.stringify({ type: 'error', message: 'unknown frame type: ' + frame.type }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleHello(ws: WebSocket, frame: { forge_token?: string; name?: string; version?: string }): Promise<void> {
|
|
160
|
+
const token = String(frame.forge_token || '').trim();
|
|
161
|
+
if (!token) {
|
|
162
|
+
ws.send(JSON.stringify({ type: 'hello_error', message: 'forge_token required' }));
|
|
163
|
+
try { ws.close(4001, 'unauthorized'); } catch {}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const ok = await isForgeTokenValid(token);
|
|
167
|
+
if (!ok) {
|
|
168
|
+
ws.send(JSON.stringify({ type: 'hello_error', message: 'token invalid or expired — log in to Forge to refresh' }));
|
|
169
|
+
try { ws.close(4001, 'unauthorized'); } catch {}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const ext_id = extIdForToken(token);
|
|
173
|
+
attachClient(ws, ext_id, frame.name);
|
|
174
|
+
ws.send(JSON.stringify({ type: 'hello_ok', ext_id }));
|
|
175
|
+
console.log(`[bridge] Extension connected: ${ext_id} (${frame.name || 'unnamed'})`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function attachClient(ws: WebSocket, ext_id: string, name?: string): void {
|
|
179
|
+
const prev = clients.get(ext_id);
|
|
180
|
+
if (prev && prev.ws !== ws) {
|
|
181
|
+
try { prev.ws.close(4000, 'replaced'); } catch {}
|
|
182
|
+
}
|
|
183
|
+
clients.set(ext_id, { ext_id, ws, connected_at: Date.now(), name });
|
|
184
|
+
ws.on('close', () => {
|
|
185
|
+
const current = clients.get(ext_id);
|
|
186
|
+
if (current && current.ws === ws) clients.delete(ext_id);
|
|
187
|
+
console.log(`[bridge] Extension disconnected: ${ext_id}`);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleRpcResult(frame: { id: string; ok: boolean; value?: unknown; error?: string }): void {
|
|
192
|
+
const pending = pendingRpcs.get(frame.id);
|
|
193
|
+
if (!pending) return;
|
|
194
|
+
pendingRpcs.delete(frame.id);
|
|
195
|
+
clearTimeout(pending.timer);
|
|
196
|
+
if (frame.ok) pending.resolve(frame.value);
|
|
197
|
+
else pending.reject(new Error(frame.error || 'rpc failed without message'));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function handleEvent(frame: { name: string; payload: unknown }): void {
|
|
201
|
+
// Reserved for extension-initiated push (tab changed, side panel opened, etc.)
|
|
202
|
+
console.log(`[bridge] Event from extension: ${frame.name}`, frame.payload);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── HTTP API ─────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
async function readBody(req: IncomingMessage): Promise<string> {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const chunks: Buffer[] = [];
|
|
210
|
+
req.on('data', (c) => chunks.push(c));
|
|
211
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
212
|
+
req.on('error', reject);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
217
|
+
res.statusCode = status;
|
|
218
|
+
res.setHeader('content-type', 'application/json');
|
|
219
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
220
|
+
res.setHeader('access-control-allow-headers', 'content-type');
|
|
221
|
+
res.end(JSON.stringify(body));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function handleHttp(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
225
|
+
if (req.method === 'OPTIONS') {
|
|
226
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
227
|
+
res.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS');
|
|
228
|
+
res.setHeader('access-control-allow-headers', 'content-type');
|
|
229
|
+
res.statusCode = 204;
|
|
230
|
+
res.end();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
235
|
+
|
|
236
|
+
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
237
|
+
return sendJson(res, 200, {
|
|
238
|
+
port: PORT,
|
|
239
|
+
forge_port: FORGE_PORT,
|
|
240
|
+
connected_extensions: clients.size,
|
|
241
|
+
pending_rpcs: pendingRpcs.size,
|
|
242
|
+
cached_tokens: tokenCache.size,
|
|
243
|
+
uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// POST /api/rpc — Forge internals call this to dispatch an RPC to the extension
|
|
248
|
+
if (req.method === 'POST' && url.pathname === '/api/rpc') {
|
|
249
|
+
try {
|
|
250
|
+
const body = JSON.parse(await readBody(req));
|
|
251
|
+
const value = await callExtension(body.method, body.params);
|
|
252
|
+
return sendJson(res, 200, { ok: true, value });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return sendJson(res, 200, { ok: false, error: (e as Error).message });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// POST /api/push — push event to all connected extensions
|
|
259
|
+
if (req.method === 'POST' && url.pathname === '/api/push') {
|
|
260
|
+
try {
|
|
261
|
+
const body = JSON.parse(await readBody(req));
|
|
262
|
+
const frame = JSON.stringify({ type: 'push', topic: body.topic, payload: body.payload });
|
|
263
|
+
let count = 0;
|
|
264
|
+
for (const client of clients.values()) {
|
|
265
|
+
try { client.ws.send(frame); count++; } catch {}
|
|
266
|
+
}
|
|
267
|
+
return sendJson(res, 200, { ok: true, delivered_to: count });
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return sendJson(res, 400, { ok: false, error: (e as Error).message });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
sendJson(res, 404, { error: 'not found' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Boot ─────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
const startTime = Date.now();
|
|
279
|
+
const httpServer = createServer((req, res) => {
|
|
280
|
+
handleHttp(req, res).catch((err) => {
|
|
281
|
+
console.error('[bridge] http error:', err);
|
|
282
|
+
sendJson(res, 500, { error: 'internal' });
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
287
|
+
wss.on('connection', (ws) => {
|
|
288
|
+
ws.on('message', (data) => handleFrame(ws, data.toString('utf-8')));
|
|
289
|
+
ws.on('error', (err) => console.warn('[bridge] ws error:', err.message));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
293
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
294
|
+
if (url.pathname !== '/ws') {
|
|
295
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
296
|
+
socket.destroy();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
300
|
+
wss.emit('connection', ws, req);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
httpServer.listen(PORT, () => {
|
|
305
|
+
console.log(`[bridge] Browser bridge listening on http://localhost:${PORT} (ws on /ws)`);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
function shutdown(): void {
|
|
309
|
+
console.log('[bridge] shutting down');
|
|
310
|
+
for (const c of clients.values()) {
|
|
311
|
+
try { c.ws.close(1001, 'server shutting down'); } catch {}
|
|
312
|
+
}
|
|
313
|
+
httpServer.close(() => process.exit(0));
|
|
314
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
315
|
+
}
|
|
316
|
+
process.on('SIGTERM', shutdown);
|
|
317
|
+
process.on('SIGINT', shutdown);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: github-api
|
|
2
|
+
name: GitHub API
|
|
3
|
+
icon: 🐙
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
GitHub REST API connector — runs entirely server-side (no browser tab
|
|
8
|
+
needed). Set settings.token to a personal access token for private
|
|
9
|
+
repos / higher rate limits; public-only endpoints work unauthenticated.
|
|
10
|
+
|
|
11
|
+
Demonstrates the `protocol: http` runtime: the connector declares the
|
|
12
|
+
request shape and Forge issues it from the server. Templates expand
|
|
13
|
+
{settings.*} (user-supplied) and {args.*} (LLM-supplied) into url,
|
|
14
|
+
headers, query, and body.
|
|
15
|
+
|
|
16
|
+
category: connector
|
|
17
|
+
mode: server-side
|
|
18
|
+
|
|
19
|
+
settings:
|
|
20
|
+
token:
|
|
21
|
+
type: string
|
|
22
|
+
label: GitHub token (optional)
|
|
23
|
+
description: Personal access token. Leave empty for public endpoints.
|
|
24
|
+
secret: true
|
|
25
|
+
|
|
26
|
+
tools:
|
|
27
|
+
get_repo:
|
|
28
|
+
description: |
|
|
29
|
+
Fetch metadata for a GitHub repository (description, stars, default
|
|
30
|
+
branch, etc.).
|
|
31
|
+
protocol: http
|
|
32
|
+
parameters:
|
|
33
|
+
repo:
|
|
34
|
+
type: string
|
|
35
|
+
required: true
|
|
36
|
+
description: 'Repository in "owner/name" form (e.g. "anthropics/anthropic-sdk-python").'
|
|
37
|
+
request:
|
|
38
|
+
method: GET
|
|
39
|
+
url: 'https://api.github.com/repos/{args.repo}'
|
|
40
|
+
headers:
|
|
41
|
+
Accept: 'application/vnd.github+json'
|
|
42
|
+
Authorization: 'Bearer {settings.token}'
|
|
43
|
+
User-Agent: 'forge-github-connector'
|
|
44
|
+
|
|
45
|
+
list_issues:
|
|
46
|
+
description: |
|
|
47
|
+
List issues in a GitHub repository. Default returns open issues
|
|
48
|
+
(most recent first). Use state="closed" or state="all" to widen.
|
|
49
|
+
protocol: http
|
|
50
|
+
parameters:
|
|
51
|
+
repo:
|
|
52
|
+
type: string
|
|
53
|
+
required: true
|
|
54
|
+
description: 'Repository in "owner/name" form.'
|
|
55
|
+
state:
|
|
56
|
+
type: string
|
|
57
|
+
description: 'open | closed | all (default: open).'
|
|
58
|
+
per_page:
|
|
59
|
+
type: number
|
|
60
|
+
description: 'Results per page (1–100, default 30).'
|
|
61
|
+
request:
|
|
62
|
+
method: GET
|
|
63
|
+
url: 'https://api.github.com/repos/{args.repo}/issues'
|
|
64
|
+
headers:
|
|
65
|
+
Accept: 'application/vnd.github+json'
|
|
66
|
+
Authorization: 'Bearer {settings.token}'
|
|
67
|
+
User-Agent: 'forge-github-connector'
|
|
68
|
+
query:
|
|
69
|
+
state: '{args.state}'
|
|
70
|
+
per_page: '{args.per_page}'
|
|
71
|
+
|
|
72
|
+
search_repos:
|
|
73
|
+
description: |
|
|
74
|
+
Search GitHub repositories by keyword. Sorted by stars desc by
|
|
75
|
+
default. Use q to pass GitHub search qualifiers
|
|
76
|
+
(e.g. "language:go forks:>100 created:>2024").
|
|
77
|
+
protocol: http
|
|
78
|
+
parameters:
|
|
79
|
+
q:
|
|
80
|
+
type: string
|
|
81
|
+
required: true
|
|
82
|
+
description: 'Search query (GitHub search syntax).'
|
|
83
|
+
request:
|
|
84
|
+
method: GET
|
|
85
|
+
url: 'https://api.github.com/search/repositories'
|
|
86
|
+
headers:
|
|
87
|
+
Accept: 'application/vnd.github+json'
|
|
88
|
+
Authorization: 'Bearer {settings.token}'
|
|
89
|
+
User-Agent: 'forge-github-connector'
|
|
90
|
+
query:
|
|
91
|
+
q: '{args.q}'
|
|
92
|
+
sort: stars
|
|
93
|
+
order: desc
|