@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.
Files changed (145) hide show
  1. package/.forge/mcp.json +8 -0
  2. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
  3. package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
  4. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
  5. package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
  6. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
  7. package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
  8. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
  9. package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
  10. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
  11. package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
  12. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
  13. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
  14. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
  15. package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
  16. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
  17. package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
  18. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
  19. package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
  20. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
  21. package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
  22. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
  23. package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
  24. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
  25. package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
  26. package/CLAUDE.md +2 -2
  27. package/RELEASE_NOTES.md +101 -5
  28. package/app/api/auth/check/route.ts +18 -0
  29. package/app/api/browser-bridge/route.ts +70 -0
  30. package/app/api/chat/sessions/[id]/events/route.ts +17 -0
  31. package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
  32. package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
  33. package/app/api/chat/sessions/[id]/route.ts +23 -0
  34. package/app/api/chat/sessions/route.ts +12 -0
  35. package/app/api/chat/temper-ping/route.ts +18 -0
  36. package/app/api/chat-proxy/[...path]/route.ts +83 -0
  37. package/app/api/connector-tool/route.ts +38 -0
  38. package/app/api/connectors/[id]/settings/route.ts +112 -0
  39. package/app/api/connectors/route.ts +108 -0
  40. package/app/api/health/tools/route.ts +14 -0
  41. package/app/api/issue-scanner-gitlab/route.ts +95 -0
  42. package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
  43. package/app/api/jobs/[id]/route.ts +31 -0
  44. package/app/api/jobs/[id]/run/route.ts +44 -0
  45. package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
  46. package/app/api/jobs/[id]/runs/route.ts +15 -0
  47. package/app/api/jobs/preview/route.ts +193 -0
  48. package/app/api/jobs/route.ts +36 -0
  49. package/app/api/notify/test/route.ts +39 -7
  50. package/app/api/pipelines/[id]/route.ts +10 -1
  51. package/app/api/pipelines/route.ts +16 -2
  52. package/app/api/plugins/route.ts +40 -8
  53. package/app/api/project-sessions/route.ts +50 -10
  54. package/app/api/settings/route.ts +13 -0
  55. package/app/chat/page.tsx +531 -0
  56. package/bin/forge-server.mjs +3 -1
  57. package/cli/chat.ts +283 -0
  58. package/cli/jobs.ts +176 -0
  59. package/cli/mw.ts +28 -1
  60. package/cli/worktree.ts +245 -0
  61. package/components/ConnectorsPanel.tsx +275 -0
  62. package/components/Dashboard.tsx +90 -37
  63. package/components/JobsView.tsx +361 -0
  64. package/components/LogViewer.tsx +12 -2
  65. package/components/PipelineView.tsx +275 -56
  66. package/components/PluginsPanel.tsx +3 -1
  67. package/components/SettingsModal.tsx +229 -40
  68. package/components/SkillsPanel.tsx +12 -4
  69. package/components/TerminalLauncher.tsx +3 -1
  70. package/components/WebTerminal.tsx +32 -9
  71. package/components/WorkspaceView.tsx +18 -10
  72. package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
  73. package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
  74. package/docs/Implementation-Plan-Browser-Agent.md +487 -0
  75. package/docs/Jobs-Design.md +240 -0
  76. package/docs/LOCAL-DEPLOY.md +3 -3
  77. package/docs/RFC-Browser-Connectors.md +509 -0
  78. package/lib/agents/index.ts +44 -6
  79. package/lib/agents/types.ts +1 -1
  80. package/lib/browser-bridge-standalone.ts +317 -0
  81. package/lib/builtin-plugins/github-api.yaml +93 -0
  82. package/lib/builtin-plugins/gitlab.yaml +860 -0
  83. package/lib/builtin-plugins/mantis.probe.js +176 -0
  84. package/lib/builtin-plugins/mantis.yaml +964 -0
  85. package/lib/builtin-plugins/pmdb.yaml +178 -0
  86. package/lib/builtin-plugins/teams.yaml +913 -0
  87. package/lib/chat/__test__/smoke.ts +30 -0
  88. package/lib/chat/agent-loop.ts +523 -0
  89. package/lib/chat/bridge-client.ts +59 -0
  90. package/lib/chat/llm/anthropic.ts +99 -0
  91. package/lib/chat/llm/index.ts +20 -0
  92. package/lib/chat/llm/openai.ts +215 -0
  93. package/lib/chat/llm/types.ts +42 -0
  94. package/lib/chat/local-memory.ts +300 -0
  95. package/lib/chat/memory-store.ts +87 -0
  96. package/lib/chat/memory-tools.ts +157 -0
  97. package/lib/chat/protocols/http.ts +118 -0
  98. package/lib/chat/protocols/shell.ts +101 -0
  99. package/lib/chat/proxy.ts +51 -0
  100. package/lib/chat/session-store.ts +272 -0
  101. package/lib/chat/telegram-bridge.ts +276 -0
  102. package/lib/chat/temper.ts +281 -0
  103. package/lib/chat/tool-dispatcher.ts +190 -0
  104. package/lib/chat/types.ts +50 -0
  105. package/lib/chat-standalone.ts +286 -0
  106. package/lib/crypto.ts +1 -1
  107. package/lib/health.ts +131 -0
  108. package/lib/help-docs/00-overview.md +2 -1
  109. package/lib/help-docs/01-settings.md +46 -25
  110. package/lib/help-docs/07-projects.md +1 -1
  111. package/lib/help-docs/10-troubleshooting.md +10 -2
  112. package/lib/help-docs/16-gitlab-autofix.md +114 -0
  113. package/lib/help-docs/17-connectors.md +322 -0
  114. package/lib/help-docs/18-chrome-mcp.md +134 -0
  115. package/lib/help-docs/19-jobs.md +140 -0
  116. package/lib/help-docs/20-mantis-bug-fix.md +115 -0
  117. package/lib/help-docs/CLAUDE.md +10 -0
  118. package/lib/init.ts +137 -50
  119. package/lib/iso-time.ts +30 -0
  120. package/lib/issue-scanner-gitlab.ts +281 -0
  121. package/lib/jobs/dispatcher.ts +217 -0
  122. package/lib/jobs/scheduler.ts +334 -0
  123. package/lib/jobs/store.ts +319 -0
  124. package/lib/jobs/types.ts +117 -0
  125. package/lib/pipeline-scheduler.ts +1 -6
  126. package/lib/pipeline.ts +790 -10
  127. package/lib/plugins/registry.ts +133 -8
  128. package/lib/plugins/templates.ts +83 -0
  129. package/lib/plugins/types.ts +140 -1
  130. package/lib/session-watcher.ts +36 -10
  131. package/lib/settings.ts +65 -33
  132. package/lib/skills.ts +3 -1
  133. package/lib/task-manager.ts +50 -22
  134. package/lib/telegram-bot.ts +71 -0
  135. package/lib/terminal-standalone.ts +58 -36
  136. package/lib/workspace/orchestrator.ts +1 -0
  137. package/middleware.ts +10 -0
  138. package/package.json +3 -2
  139. package/scripts/bench/README.md +1 -1
  140. package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
  141. package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
  142. package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
  143. package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
  144. package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
  145. package/src/core/db/database.ts +21 -12
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * Chat backend standalone — HTTP server on $CHAT_PORT (default 8408).
4
+ *
5
+ * Why a standalone:
6
+ * - Exclusive writer to chat_sessions / chat_messages tables, mirroring
7
+ * the workspace daemon pattern (avoids multi-writer races across
8
+ * Next.js workers).
9
+ * - One canonical endpoint for every surface: web `/chat` tab (via Next
10
+ * proxy), `forge chat` CLI, Telegram bot, extension side panel.
11
+ * - LLM streams can run for tens of seconds without occupying a Next
12
+ * worker for the whole turn.
13
+ *
14
+ * Loopback-only — Next.js routes proxy requests in; CLI/Telegram talk
15
+ * directly. Auth happens at the Next.js / Telegram layer.
16
+ *
17
+ * Routes:
18
+ * GET /api/status
19
+ * GET /api/sessions
20
+ * POST /api/sessions { title?, model?, provider?, system_prompt?, meta? }
21
+ * GET /api/sessions/:id?after_ts=&limit=
22
+ * PATCH /api/sessions/:id
23
+ * DELETE /api/sessions/:id
24
+ * POST /api/sessions/:id/messages { text } → 202 { topic }
25
+ * GET /api/sessions/:id/events SSE stream of agent events
26
+ *
27
+ * Fanout:
28
+ * Each agent event is delivered to (a) local SSE subscribers and
29
+ * (b) the browser bridge as `chat:<session_id>` push. Browser surfaces
30
+ * keep their existing WS path; HTTP-only surfaces use SSE.
31
+ */
32
+
33
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
34
+ import {
35
+ createSession, getSession, listSessions, updateSession, deleteSession, listMessages,
36
+ clearSessionMessages, ensureMainSession, forkSession,
37
+ } from './chat/session-store';
38
+ import { runTurn, type AgentEvent } from './chat/agent-loop';
39
+ import { bridgePush } from './chat/bridge-client';
40
+
41
+ const PORT = Number(process.env.CHAT_PORT) || 8408;
42
+ const startTime = Date.now();
43
+
44
+ // ─── SSE subscriber registry ──────────────────────────────
45
+
46
+ const sseSubs = new Map<string, Set<ServerResponse>>(); // session_id → subscribers
47
+
48
+ function addSseSub(sessionId: string, res: ServerResponse): void {
49
+ let set = sseSubs.get(sessionId);
50
+ if (!set) { set = new Set(); sseSubs.set(sessionId, set); }
51
+ set.add(res);
52
+ }
53
+
54
+ function removeSseSub(sessionId: string, res: ServerResponse): void {
55
+ const set = sseSubs.get(sessionId);
56
+ if (!set) return;
57
+ set.delete(res);
58
+ if (set.size === 0) sseSubs.delete(sessionId);
59
+ }
60
+
61
+ function fanoutEvent(sessionId: string, event: AgentEvent): void {
62
+ // (a) browser bridge push — extension side panels listen on this topic.
63
+ void bridgePush(`chat:${sessionId}`, event);
64
+ // (b) local SSE subscribers — CLI, web tab, future surfaces.
65
+ const set = sseSubs.get(sessionId);
66
+ if (!set || set.size === 0) return;
67
+ const frame = `data: ${JSON.stringify(event)}\n\n`;
68
+ for (const res of set) {
69
+ try { res.write(frame); } catch {}
70
+ }
71
+ }
72
+
73
+ // ─── HTTP helpers ─────────────────────────────────────────
74
+
75
+ async function readJson(req: IncomingMessage): Promise<any> {
76
+ return new Promise((resolve, reject) => {
77
+ const chunks: Buffer[] = [];
78
+ req.on('data', (c) => chunks.push(c));
79
+ req.on('end', () => {
80
+ const raw = Buffer.concat(chunks).toString('utf-8');
81
+ if (!raw) return resolve({});
82
+ try { resolve(JSON.parse(raw)); } catch (e) { reject(e); }
83
+ });
84
+ req.on('error', reject);
85
+ });
86
+ }
87
+
88
+ function sendJson(res: ServerResponse, status: number, body: unknown): void {
89
+ res.statusCode = status;
90
+ res.setHeader('content-type', 'application/json');
91
+ res.end(JSON.stringify(body));
92
+ }
93
+
94
+ // ─── Route handlers ───────────────────────────────────────
95
+
96
+ async function handleSessionsList(_req: IncomingMessage, res: ServerResponse, url: URL): Promise<void> {
97
+ const limit = Math.min(200, Math.max(1, Number(url.searchParams.get('limit') || 50)));
98
+ sendJson(res, 200, { sessions: listSessions(limit) });
99
+ }
100
+
101
+ async function handleSessionCreate(req: IncomingMessage, res: ServerResponse): Promise<void> {
102
+ const body = await readJson(req);
103
+ const session = createSession({
104
+ title: body.title,
105
+ model: body.model,
106
+ provider: body.provider,
107
+ system_prompt: body.system_prompt,
108
+ meta: body.meta,
109
+ });
110
+ sendJson(res, 200, { session });
111
+ }
112
+
113
+ async function handleSessionGet(_req: IncomingMessage, res: ServerResponse, id: string, url: URL): Promise<void> {
114
+ const session = getSession(id);
115
+ if (!session) return sendJson(res, 404, { error: 'session not found' });
116
+ const after_ts = Number(url.searchParams.get('after_ts') || 0);
117
+ const limit = Math.min(2000, Math.max(1, Number(url.searchParams.get('limit') || 500)));
118
+ sendJson(res, 200, { session, messages: listMessages(id, { after_ts, limit }) });
119
+ }
120
+
121
+ async function handleSessionPatch(req: IncomingMessage, res: ServerResponse, id: string): Promise<void> {
122
+ const session = getSession(id);
123
+ if (!session) return sendJson(res, 404, { error: 'session not found' });
124
+ const body = await readJson(req);
125
+ const ok = updateSession(id, {
126
+ title: body.title,
127
+ model: body.model,
128
+ provider: body.provider,
129
+ system_prompt: body.system_prompt,
130
+ meta: body.meta,
131
+ });
132
+ sendJson(res, 200, { ok, session: getSession(id) });
133
+ }
134
+
135
+ async function handleSessionDelete(_req: IncomingMessage, res: ServerResponse, id: string): Promise<void> {
136
+ // Refuse to delete the persistent main session — the UI is allowed to
137
+ // "clear" it (drop messages, keep the session) via DELETE /messages.
138
+ const session = getSession(id);
139
+ if (session?.meta && (session.meta as any).kind === 'main') {
140
+ return sendJson(res, 400, { error: 'cannot delete the main session — use DELETE /api/sessions/:id/messages to clear it' });
141
+ }
142
+ const ok = deleteSession(id);
143
+ sendJson(res, 200, { ok });
144
+ }
145
+
146
+ async function handleSessionClearMessages(_req: IncomingMessage, res: ServerResponse, id: string): Promise<void> {
147
+ const session = getSession(id);
148
+ if (!session) return sendJson(res, 404, { error: 'session not found' });
149
+ const removed = clearSessionMessages(id);
150
+ sendJson(res, 200, { ok: true, removed });
151
+ }
152
+
153
+ async function handleSessionFork(req: IncomingMessage, res: ServerResponse, id: string): Promise<void> {
154
+ const body = await readJson(req).catch(() => ({}));
155
+ const title = body?.title ? String(body.title) : undefined;
156
+ const upToTs = typeof body?.up_to_ts === 'number' ? body.up_to_ts : undefined;
157
+ const fork = forkSession(id, { title, upToTs });
158
+ if (!fork) return sendJson(res, 404, { error: 'source session not found' });
159
+ sendJson(res, 200, { ok: true, session: fork });
160
+ }
161
+
162
+ async function handleMessagePost(req: IncomingMessage, res: ServerResponse, id: string): Promise<void> {
163
+ const session = getSession(id);
164
+ if (!session) return sendJson(res, 404, { error: 'session not found' });
165
+ const body = await readJson(req);
166
+ const text = String(body?.text || '').trim();
167
+ if (!text) return sendJson(res, 400, { error: 'text is required' });
168
+
169
+ const startedAt = Date.now();
170
+ void runTurn({
171
+ sessionId: id,
172
+ userText: text,
173
+ callbacks: { onEvent: (e: AgentEvent) => fanoutEvent(id, e) },
174
+ }).then((r) => {
175
+ if (!r.ok) fanoutEvent(id, { type: 'error', data: { error: r.error || 'unknown' } });
176
+ console.log(`[chat] turn ${id.slice(0, 8)} done in ${Date.now() - startedAt}ms ok=${r.ok}`);
177
+ }).catch((err) => {
178
+ fanoutEvent(id, { type: 'error', data: { error: (err as Error).message } });
179
+ console.error('[chat] turn error', err);
180
+ });
181
+
182
+ sendJson(res, 202, { accepted: true, topic: `chat:${id}` });
183
+ }
184
+
185
+ function handleEventsSse(_req: IncomingMessage, res: ServerResponse, id: string): void {
186
+ // Verify the session exists before opening the stream.
187
+ const session = getSession(id);
188
+ if (!session) return sendJson(res, 404, { error: 'session not found' });
189
+
190
+ res.statusCode = 200;
191
+ res.setHeader('content-type', 'text/event-stream');
192
+ res.setHeader('cache-control', 'no-cache, no-transform');
193
+ res.setHeader('connection', 'keep-alive');
194
+ res.setHeader('x-accel-buffering', 'no'); // disable proxy buffering
195
+ res.write(`: subscribed to chat:${id}\n\n`);
196
+
197
+ addSseSub(id, res);
198
+
199
+ // Heartbeat every 25s keeps proxies from idling out.
200
+ const hb = setInterval(() => {
201
+ try { res.write(`: ping ${Date.now()}\n\n`); } catch {}
202
+ }, 25_000);
203
+ hb.unref?.();
204
+
205
+ const cleanup = () => {
206
+ clearInterval(hb);
207
+ removeSseSub(id, res);
208
+ try { res.end(); } catch {}
209
+ };
210
+ res.on('close', cleanup);
211
+ res.on('error', cleanup);
212
+ }
213
+
214
+ // ─── Router ───────────────────────────────────────────────
215
+
216
+ async function route(req: IncomingMessage, res: ServerResponse): Promise<void> {
217
+ const url = new URL(req.url || '/', 'http://localhost');
218
+ const m = req.method || 'GET';
219
+
220
+ if (m === 'GET' && url.pathname === '/api/status') {
221
+ return sendJson(res, 200, {
222
+ port: PORT,
223
+ uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
224
+ active_sse_sessions: sseSubs.size,
225
+ sse_subscribers: [...sseSubs.values()].reduce((n, s) => n + s.size, 0),
226
+ });
227
+ }
228
+
229
+ if (m === 'GET' && url.pathname === '/api/sessions') return handleSessionsList(req, res, url);
230
+ if (m === 'POST' && url.pathname === '/api/sessions') return handleSessionCreate(req, res);
231
+ if (m === 'GET' && url.pathname === '/api/sessions/main') {
232
+ return sendJson(res, 200, { session: ensureMainSession() });
233
+ }
234
+
235
+ const detail = /^\/api\/sessions\/([^/]+)$/.exec(url.pathname);
236
+ if (detail) {
237
+ const id = detail[1]!;
238
+ if (m === 'GET') return handleSessionGet(req, res, id, url);
239
+ if (m === 'PATCH') return handleSessionPatch(req, res, id);
240
+ if (m === 'DELETE') return handleSessionDelete(req, res, id);
241
+ }
242
+
243
+ const messages = /^\/api\/sessions\/([^/]+)\/messages$/.exec(url.pathname);
244
+ if (messages && m === 'POST') return handleMessagePost(req, res, messages[1]!);
245
+ if (messages && m === 'DELETE') return handleSessionClearMessages(req, res, messages[1]!);
246
+
247
+ const fork = /^\/api\/sessions\/([^/]+)\/fork$/.exec(url.pathname);
248
+ if (fork && m === 'POST') return handleSessionFork(req, res, fork[1]!);
249
+
250
+ const events = /^\/api\/sessions\/([^/]+)\/events$/.exec(url.pathname);
251
+ if (events && m === 'GET') return handleEventsSse(req, res, events[1]!);
252
+
253
+ sendJson(res, 404, { error: 'not found' });
254
+ }
255
+
256
+ // ─── Boot ─────────────────────────────────────────────────
257
+
258
+ const httpServer = createServer((req, res) => {
259
+ route(req, res).catch((err) => {
260
+ console.error('[chat] http error:', err);
261
+ try { sendJson(res, 500, { error: 'internal' }); } catch {}
262
+ });
263
+ });
264
+
265
+ httpServer.listen(PORT, '127.0.0.1', () => {
266
+ console.log(`[chat] Chat standalone listening on http://127.0.0.1:${PORT}`);
267
+ // Make sure exactly one persistent main session exists. The UI uses
268
+ // meta.kind === 'main' to pin it, refuse delete, and route the
269
+ // top-level "clear" button to DELETE /messages instead.
270
+ try {
271
+ const main = ensureMainSession();
272
+ console.log(`[chat] Main session: ${main.id.slice(0, 8)} "${main.title}"`);
273
+ } catch (e) { console.warn('[chat] ensureMainSession failed:', (e as Error).message); }
274
+ });
275
+
276
+ function shutdown(): void {
277
+ console.log('[chat] shutting down');
278
+ for (const set of sseSubs.values()) {
279
+ for (const res of set) { try { res.end(); } catch {} }
280
+ }
281
+ sseSubs.clear();
282
+ httpServer.close(() => process.exit(0));
283
+ setTimeout(() => process.exit(0), 2000).unref();
284
+ }
285
+ process.on('SIGTERM', shutdown);
286
+ process.on('SIGINT', shutdown);
package/lib/crypto.ts CHANGED
@@ -63,5 +63,5 @@ export function hashSecret(value: string): string {
63
63
  }
64
64
 
65
65
  /** Secret field names in settings */
66
- export const SECRET_FIELDS = ['telegramBotToken', 'telegramTunnelPassword'] as const;
66
+ export const SECRET_FIELDS = ['telegramBotToken', 'telegramTunnelPassword', 'temperKey'] as const;
67
67
  export type SecretField = typeof SECRET_FIELDS[number];
package/lib/health.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * External-tool health checks. Surfaced two ways:
3
+ * - On startup (lib/init.ts) — one log line per tool with install hint
4
+ * if missing, so server logs make the problem obvious
5
+ * - Via `GET /api/health/tools` — the Pipelines view uses this to show
6
+ * a banner when a tool needed by a built-in workflow isn't installed
7
+ *
8
+ * Add new tools by extending CHECKS. Tools are marked "optional" when a
9
+ * subset of Forge features needs them — the absence isn't an error, just
10
+ * a heads-up that those features won't work.
11
+ */
12
+
13
+ import { execSync } from 'node:child_process';
14
+
15
+ export interface ToolStatus {
16
+ /** binary name as invoked from PATH */
17
+ name: string;
18
+ /** human-readable label for UI */
19
+ label: string;
20
+ /** what part of Forge needs this — shown in the warning */
21
+ needed_for: string;
22
+ /** absolute path if installed, '' otherwise */
23
+ path: string;
24
+ /** parsed version string if we could pull one */
25
+ version: string;
26
+ /** true iff `which <name>` found it */
27
+ installed: boolean;
28
+ /** copy-pasteable install / auth instructions when missing */
29
+ install_hint: string;
30
+ }
31
+
32
+ interface Check {
33
+ name: string;
34
+ label: string;
35
+ needed_for: string;
36
+ versionFlag?: string; // arg to invoke for version probe (default --version)
37
+ install_hint: string;
38
+ }
39
+
40
+ const CHECKS: Check[] = [
41
+ {
42
+ name: 'glab',
43
+ label: 'GitLab CLI',
44
+ needed_for: 'gitlab-issue-fix-and-review + mantis-bug-fix-and-mr pipelines (push branches + open MRs)',
45
+ install_hint: [
46
+ 'macOS: brew install glab',
47
+ 'Linux: https://gitlab.com/gitlab-org/cli/-/releases (deb/rpm/tar)',
48
+ 'Windows: winget install GitLab.cli',
49
+ 'Then auth once per host: glab auth login --hostname <your-gitlab.example.com>',
50
+ ].join('\n'),
51
+ },
52
+ {
53
+ name: 'gh',
54
+ label: 'GitHub CLI',
55
+ needed_for: 'issue-fix-and-review pipeline + GitHub issue scanner',
56
+ install_hint: [
57
+ 'macOS: brew install gh',
58
+ 'Linux: https://cli.github.com/manual/installation',
59
+ 'Then auth once: gh auth login',
60
+ ].join('\n'),
61
+ },
62
+ {
63
+ name: 'git',
64
+ label: 'git',
65
+ needed_for: 'every pipeline (worktree, fetch, push)',
66
+ install_hint: [
67
+ 'macOS: xcode-select --install (or brew install git)',
68
+ 'Linux: apt install git / dnf install git',
69
+ 'Windows: https://git-scm.com/download/win',
70
+ ].join('\n'),
71
+ },
72
+ {
73
+ name: 'jq',
74
+ label: 'jq',
75
+ needed_for: 'gitlab-issue-fix-and-review (parses glab JSON)',
76
+ install_hint: [
77
+ 'macOS: brew install jq',
78
+ 'Linux: apt install jq / dnf install jq',
79
+ ].join('\n'),
80
+ },
81
+ ];
82
+
83
+ function which(bin: string): string {
84
+ try {
85
+ return execSync(`which ${bin}`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
86
+ } catch {
87
+ return '';
88
+ }
89
+ }
90
+
91
+ function version(bin: string, flag = '--version'): string {
92
+ try {
93
+ const out = execSync(`${bin} ${flag}`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
94
+ // First line, usually like "git version 2.42.0" or "glab version 1.40.0 (...)"
95
+ return out.split('\n')[0] || '';
96
+ } catch {
97
+ return '';
98
+ }
99
+ }
100
+
101
+ export function checkTools(): ToolStatus[] {
102
+ return CHECKS.map((c) => {
103
+ const path = which(c.name);
104
+ return {
105
+ name: c.name,
106
+ label: c.label,
107
+ needed_for: c.needed_for,
108
+ path,
109
+ version: path ? version(c.name, c.versionFlag) : '',
110
+ installed: !!path,
111
+ install_hint: c.install_hint,
112
+ };
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Log a one-line summary per tool to the startup console. For missing
118
+ * ones, also log the install hint so users see it without poking at the
119
+ * UI.
120
+ */
121
+ export function logToolStatus(): void {
122
+ const results = checkTools();
123
+ for (const r of results) {
124
+ if (r.installed) {
125
+ console.log(`[tools] ✓ ${r.name.padEnd(6)} ${r.version || r.path}`);
126
+ } else {
127
+ console.warn(`[tools] ✗ ${r.name.padEnd(6)} NOT FOUND — ${r.needed_for}`);
128
+ for (const line of r.install_hint.split('\n')) console.warn(` ${line}`);
129
+ }
130
+ }
131
+ }
@@ -16,7 +16,8 @@ Forge is a self-hosted Vibe Coding platform for Claude Code. It provides a brows
16
16
  | **Telegram Bot** | Mobile control — submit tasks, receive notifications |
17
17
  | **Remote Access** | One-click Cloudflare tunnel for remote browsing |
18
18
  | **GitHub Issue Auto-fix** | Scan issues, auto-fix, create PRs |
19
- | **Memory (optional)** | Code graph + knowledge via `@aion0/temper` MCP server |
19
+ | **Memory** | Long-term chat memory. Configure `@aion0/temper` for semantic + graph search, or leave URL/key blank to use the built-in local SQLite store (same block/episode API, keyword search). |
20
+ | **Web Chat (simplified)** | `/chat` — minimal in-browser chat UI for use without the extension. Sessions, SSE streaming, memory badge. Full connector UX still lives in the extension. |
20
21
  | **IDE Plugins** | First-party VSCode extension and IntelliJ plugin — drive workspaces, agents, terminals, pipelines and docs from inside the editor (see `13-ide-plugins.md`) |
21
22
 
22
23
  ## Quick Start
@@ -61,7 +61,7 @@ The `cliType` field determines how Forge interacts with the agent:
61
61
  | CLI Type | Session Support | Resume | Skip Permissions |
62
62
  |----------|----------------|--------|------------------|
63
63
  | `claude-code` | Yes (session files) | `-c` / `--resume <id>` | `--dangerously-skip-permissions` |
64
- | `codex` | No | — | `--full-auto` |
64
+ | `codex` | No | — | `--dangerously-bypass-approvals-and-sandbox` |
65
65
  | `aider` | No | — | `--yes` |
66
66
  | `generic` | No | — | — |
67
67
 
@@ -81,7 +81,7 @@ agents:
81
81
  enabled: true
82
82
  cliType: codex
83
83
  requiresTTY: true
84
- skipPermissionsFlag: --full-auto
84
+ skipPermissionsFlag: --dangerously-bypass-approvals-and-sandbox
85
85
  ```
86
86
 
87
87
  ## Agent Profiles
@@ -103,14 +103,14 @@ Profiles are reusable configurations that extend a base agent with custom enviro
103
103
 
104
104
  ```yaml
105
105
  agents:
106
- forti-k2:
106
+ forge-k2:
107
107
  base: claude
108
- name: Forti K2
109
- model: forti-k2
108
+ name: Forge K2
109
+ model: forge-k2
110
110
  env:
111
111
  ANTHROPIC_AUTH_TOKEN: sk-xxx
112
112
  ANTHROPIC_BASE_URL: http://my-server:7001/
113
- ANTHROPIC_SMALL_FAST_MODEL: forti-k2
113
+ ANTHROPIC_SMALL_FAST_MODEL: forge-k2
114
114
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "true"
115
115
  DISABLE_TELEMETRY: "true"
116
116
  ```
@@ -143,31 +143,51 @@ In the agent configuration, set the `profile` field to link a profile:
143
143
  ```yaml
144
144
  agents:
145
145
  claude:
146
- profile: forti-k2 # Claude will use forti-k2's env/model when launched
146
+ profile: forge-k2 # Claude will use forge-k2's env/model when launched
147
147
  ```
148
148
 
149
149
  This applies the profile's environment variables and model override whenever that agent is launched in a terminal.
150
150
 
151
- ## API Providers
151
+ ## API agent profiles
152
152
 
153
- Configure API keys for direct API access (used by API-backend smiths):
153
+ API access (used by API-backend smiths and the chat backend) is configured
154
+ as agent profiles with `type: api`. The same profile is reused everywhere
155
+ — no separate provider/key duplication.
154
156
 
155
157
  ```yaml
156
- providers:
157
- anthropic:
158
- apiKey: sk-ant-... # encrypted on save
159
- defaultModel: claude-sonnet-4-6
160
- enabled: true
161
- google:
162
- apiKey: AIza...
163
- defaultModel: gemini-2.0-flash
164
- enabled: true
165
- openai:
166
- apiKey: sk-...
167
- enabled: false
158
+ agents:
159
+ my-litellm:
160
+ type: api
161
+ name: My LiteLLM
162
+ provider: litellm # display label
163
+ model: claude-sonnet-4-6
164
+ apiKey: sk-... # encrypted on save
165
+ baseUrl: http://127.0.0.1:4000/v1 # LiteLLM / Azure / self-hosted proxy
166
+
167
+ direct-anthropic:
168
+ type: api
169
+ name: Anthropic Direct
170
+ provider: anthropic
171
+ model: claude-haiku-4-5-20251001
172
+ apiKey: sk-ant-...
173
+
174
+ chatAgent: my-litellm # which profile Forge's chat backend uses
168
175
  ```
169
176
 
170
- Provider API keys are encrypted with AES-256-GCM. The UI shows masked values (••••••••).
177
+ The chat backend (extension chat, `forge chat`, Telegram `/chat`) picks
178
+ `settings.chatAgent` by default; a session can override via
179
+ `Session.provider = <profile-id>`.
180
+
181
+ Provider → adapter mapping for chat:
182
+ - `provider: anthropic` (or `claude`) → Anthropic Messages API protocol
183
+ - everything else → OpenAI-compatible protocol (LiteLLM, Grok, Google via
184
+ LiteLLM, OpenAI itself, Azure)
185
+
186
+ `baseUrl` falls back to `env.ANTHROPIC_BASE_URL` / `env.OPENAI_BASE_URL` /
187
+ `env.OPENAI_API_BASE` if not set directly. `apiKey` falls back to
188
+ `env.ANTHROPIC_API_KEY` / `env.ANTHROPIC_AUTH_TOKEN` /
189
+ `env.OPENAI_API_KEY`. This makes a single profile usable both by a CLI
190
+ (reads `env`) and by chat (reads the top-level field).
171
191
 
172
192
  ## Admin Password
173
193
 
@@ -177,7 +197,7 @@ Provider API keys are encrypted with AES-256-GCM. The UI shows masked values (
177
197
 
178
198
  ## Encrypted Fields
179
199
 
180
- `telegramBotToken` and `telegramTunnelPassword` are encrypted with AES-256-GCM. Agent/provider `apiKey` fields are also encrypted. The encryption key is stored at `~/.forge/data/.encrypt-key`.
200
+ `telegramBotToken` and `telegramTunnelPassword` are encrypted with AES-256-GCM. Agent profile `apiKey` fields are also encrypted. The encryption key is stored at `~/.forge/data/.encrypt-key`.
181
201
 
182
202
  ## Settings UI
183
203
 
@@ -188,8 +208,9 @@ The Settings modal has these sections:
188
208
  | **Project Roots** | Directories to scan for projects |
189
209
  | **Document Roots** | Markdown/Obsidian vault paths |
190
210
  | **Agents** | Detected CLI agents + configuration |
191
- | **Profiles** | Agent profiles with env/model overrides |
192
- | **Providers** | API provider keys and defaults |
211
+ | **Profiles** | Agent profiles (CLI + API) — API profiles also power Forge chat |
212
+ | **Memory (Temper)** | Long-term memory backend for the chat agent. When Temper URL+key are blank, chat falls back to a local SQLite store with the same block/episode tools (keyword search only — no semantic/graph search). |
213
+ | **Chat backend** | Pick the default API profile for chat (extension / CLI / Telegram) |
193
214
  | **Telegram** | Bot token, chat ID, notification toggles |
194
215
  | **Display** | Name, email |
195
216
  | **Other** | Skip permissions, skills repo URL, notification retention |
@@ -81,7 +81,7 @@ There are two ways to open a terminal for a project:
81
81
 
82
82
  When selecting an agent or profile for a terminal:
83
83
  - **Base agents** (Claude, Codex, Aider): Use their default configuration
84
- - **Profiles** (e.g., Forti K2, Claude Opus): Apply the profile's environment variables and model override
84
+ - **Profiles** (e.g., Forge K2, Claude Opus): Apply the profile's environment variables and model override
85
85
  - Environment variables are exported in the terminal before launching the CLI
86
86
  - Model is passed via `--model` flag (for claude-code agents)
87
87
 
@@ -2,12 +2,20 @@
2
2
 
3
3
  ## Common Issues
4
4
 
5
- ### "fork failed: Device not configured" (macOS)
6
- PTY device limit exhausted:
5
+ ### "fork failed: Device not configured" / "posix_spawnp failed" (macOS)
6
+ Symptom: after the server has been running for a long time, opening a new terminal fails with `Failed to create tmux session: posix_spawnp failed`.
7
+
8
+ Forge ≥ 0.5.46 auto-recovers by killing idle sessions on PTY exhaustion (both random-name and fixed-name `create` paths go through the same recovery). On older versions, or if the macOS PTY pool is genuinely too small, raise the limit:
7
9
  ```bash
8
10
  sudo sysctl kern.tty.ptmx_max=2048
9
11
  echo 'kern.tty.ptmx_max=2048' | sudo tee -a /etc/sysctl.conf
10
12
  ```
13
+ Manual cleanup if needed:
14
+ ```bash
15
+ tmux list-sessions
16
+ tmux kill-session -t <name> # kill specific
17
+ tmux kill-server # nuclear option — kills ALL tmux sessions
18
+ ```
11
19
 
12
20
  ### Session cookie invalid after restart
13
21
  Fix AUTH_SECRET so it persists: