@aion0/forge 0.1.0 → 0.1.2

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 CHANGED
@@ -31,6 +31,12 @@ No API keys required. Forge runs on your existing Claude Code subscription.
31
31
 
32
32
  ```bash
33
33
  npm install -g @aion0/forge
34
+
35
+ # Start the server
36
+ forge-server
37
+
38
+ # Or in development mode
39
+ forge-server --dev
34
40
  ```
35
41
 
36
42
  ### From source
@@ -39,38 +45,12 @@ npm install -g @aion0/forge
39
45
  git clone https://github.com/aiwatching/forge.git
40
46
  cd forge
41
47
  pnpm install
42
- pnpm build
48
+ pnpm dev
43
49
  ```
44
50
 
45
51
  ## Quick Start
46
52
 
47
- ### 1. Create config
48
-
49
- Create `.env.local` in the project root:
50
-
51
- ```env
52
- # Auth (generate a random string, e.g. openssl rand -hex 32)
53
- AUTH_SECRET=<random-string>
54
- AUTH_TRUST_HOST=true
55
-
56
- # Optional: Google OAuth for production
57
- # GOOGLE_CLIENT_ID=...
58
- # GOOGLE_CLIENT_SECRET=...
59
- ```
60
-
61
- > **API keys are not required.** Forge uses your local Claude Code CLI, which runs on your Anthropic subscription. If you want to use the built-in multi-model chat feature, you can optionally add provider keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `XAI_API_KEY`) later.
62
-
63
- ### 2. Start the server
64
-
65
- ```bash
66
- # Development
67
- pnpm dev
68
-
69
- # Production
70
- pnpm build && pnpm start
71
- ```
72
-
73
- ### 3. Log in
53
+ ### 1. Log in
74
54
 
75
55
  Open `http://localhost:3000`. A login password is auto-generated and printed in the console:
76
56
 
@@ -252,6 +232,20 @@ notifyOnFailure: true
252
232
  | Tunnel | Cloudflare (cloudflared) |
253
233
  | Bot | Telegram Bot API (extensible) |
254
234
 
235
+ ## Troubleshooting
236
+
237
+ ### macOS: "fork failed: Device not configured"
238
+
239
+ This means the system ran out of pseudo-terminal (PTY) devices. macOS defaults to 511, which can be tight when running IDEs and many terminal sessions. Increase the limit:
240
+
241
+ ```bash
242
+ # Temporary (until reboot)
243
+ sudo sysctl kern.tty.ptmx_max=2048
244
+
245
+ # Permanent
246
+ echo 'kern.tty.ptmx_max=2048' | sudo tee -a /etc/sysctl.conf
247
+ ```
248
+
255
249
  ## Roadmap
256
250
 
257
251
  - [ ] **Multi-Agent Workflow** — DAG-based pipelines where multiple Claude Code instances collaborate, passing outputs between nodes with conditional routing and parallel execution. See [docs/roadmap-multi-agent-workflow.md](docs/roadmap-multi-agent-workflow.md).
@@ -42,19 +42,6 @@ export default function LoginPage() {
42
42
  </button>
43
43
  </form>
44
44
 
45
- <div className="flex items-center gap-3 text-[var(--text-secondary)] text-xs">
46
- <div className="flex-1 h-px bg-[var(--border)]" />
47
- <span>or</span>
48
- <div className="flex-1 h-px bg-[var(--border)]" />
49
- </div>
50
-
51
- {/* Google OAuth */}
52
- <button
53
- onClick={() => signIn('google', { callbackUrl: window.location.origin + '/' })}
54
- className="w-full py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-sm text-[var(--text-primary)] hover:bg-[var(--border)] transition-colors"
55
- >
56
- Sign in with Google
57
- </button>
58
45
  </div>
59
46
  </div>
60
47
  );
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge-server — Start the Forge web platform.
4
+ *
5
+ * Usage:
6
+ * forge-server Start in production mode (builds first if needed)
7
+ * forge-server --dev Start in development mode
8
+ */
9
+
10
+ import { execSync, spawn } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { join, dirname } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = join(__dirname, '..');
17
+ const isDev = process.argv.includes('--dev');
18
+
19
+ process.chdir(ROOT);
20
+
21
+ if (isDev) {
22
+ console.log('[forge] Starting in development mode...');
23
+ const child = spawn('npx', ['next', 'dev', '--turbopack'], {
24
+ cwd: ROOT,
25
+ stdio: 'inherit',
26
+ env: { ...process.env },
27
+ });
28
+ child.on('exit', (code) => process.exit(code || 0));
29
+ } else {
30
+ // Build if .next doesn't exist
31
+ if (!existsSync(join(ROOT, '.next'))) {
32
+ console.log('[forge] Building...');
33
+ execSync('npx next build', { cwd: ROOT, stdio: 'inherit' });
34
+ }
35
+ console.log('[forge] Starting server...');
36
+ const child = spawn('npx', ['next', 'start'], {
37
+ cwd: ROOT,
38
+ stdio: 'inherit',
39
+ env: { ...process.env },
40
+ });
41
+ child.on('exit', (code) => process.exit(code || 0));
42
+ }
package/cli/mw.ts CHANGED
File without changes
@@ -836,7 +836,7 @@ const MemoTerminalPane = memo(function TerminalPane({
836
836
  fontSize: 13,
837
837
  fontFamily: 'Menlo, Monaco, "Courier New", monospace',
838
838
  scrollback: 10000,
839
- logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
839
+ logger: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
840
840
  theme: {
841
841
  background: '#1a1a2e',
842
842
  foreground: '#e0e0e0',
@@ -920,8 +920,11 @@ const MemoTerminalPane = memo(function TerminalPane({
920
920
  const sn = sessionNameRef.current;
921
921
  if (sn) {
922
922
  socket.send(JSON.stringify({ type: 'attach', sessionName: sn, cols, rows }));
923
- } else {
923
+ } else if (createRetries < MAX_CREATE_RETRIES) {
924
+ createRetries++;
924
925
  socket.send(JSON.stringify({ type: 'create', cols, rows }));
926
+ } else {
927
+ term.write('\r\n\x1b[91m[failed to create session — check server logs]\x1b[0m\r\n');
925
928
  }
926
929
  }
927
930
  };
@@ -957,15 +960,7 @@ const MemoTerminalPane = memo(function TerminalPane({
957
960
  }, 500);
958
961
  }
959
962
  } else if (msg.type === 'error') {
960
- if (!connectedSession && createRetries < MAX_CREATE_RETRIES) {
961
- createRetries++;
962
- term.write(`\r\n\x1b[93m[${msg.message || 'error'} — retry ${createRetries}/${MAX_CREATE_RETRIES}...]\x1b[0m\r\n`);
963
- if (ws?.readyState === WebSocket.OPEN) {
964
- ws.send(JSON.stringify({ type: 'create', cols: term.cols, rows: term.rows }));
965
- }
966
- } else {
967
- term.write(`\r\n\x1b[93m[${msg.message || 'error'}]\x1b[0m\r\n`);
968
- }
963
+ term.write(`\r\n\x1b[93m[${msg.message || 'error'}]\x1b[0m\r\n`);
969
964
  } else if (msg.type === 'exit') {
970
965
  term.write('\r\n\x1b[90m[session ended]\x1b[0m\r\n');
971
966
  }
@@ -222,8 +222,7 @@ function executeTask(task: Task): Promise<void> {
222
222
 
223
223
  // Resolve the actual claude CLI script path (claude is a symlink to a .js file)
224
224
  const resolvedClaude = resolveClaudePath(claudePath);
225
- console.log(`[task-runner] Spawning: ${resolvedClaude.cmd} ${resolvedClaude.prefix.concat(args).join(' ')}`);
226
- console.log(`[task-runner] CWD: ${task.projectPath}`);
225
+ console.log(`[task] ${task.projectName}: "${task.prompt.slice(0, 60)}..."`);
227
226
 
228
227
  const child = spawn(resolvedClaude.cmd, [...resolvedClaude.prefix, ...args], {
229
228
  cwd: task.projectPath,
@@ -243,7 +242,7 @@ function executeTask(task: Task): Promise<void> {
243
242
  });
244
243
 
245
244
  child.stdout?.on('data', (data: Buffer) => {
246
- console.log(`[task-runner] stdout chunk: ${data.toString().slice(0, 200)}`);
245
+ // stdout chunk processing (silent)
247
246
 
248
247
  // Check if cancelled
249
248
  if (getTask(task.id)?.status === 'cancelled') {
@@ -275,14 +274,14 @@ function executeTask(task: Task): Promise<void> {
275
274
 
276
275
  child.stderr?.on('data', (data: Buffer) => {
277
276
  const text = data.toString().trim();
278
- console.error(`[task-runner] stderr: ${text.slice(0, 300)}`);
277
+ // stderr logged to task log only
279
278
  if (text) {
280
279
  appendLog(task.id, { type: 'system', subtype: 'error', content: text, timestamp: new Date().toISOString() });
281
280
  }
282
281
  });
283
282
 
284
283
  child.on('exit', (code, signal) => {
285
- console.log(`[task-runner] Process exited: code=${code}, signal=${signal}`);
284
+ // Process exit handled below
286
285
  // Process remaining buffer
287
286
  if (buffer.trim()) {
288
287
  try {
@@ -511,7 +510,7 @@ function startMonitorTask(task: Task) {
511
510
  const stopTail = tailSessionFile(fp, (newEntries) => {
512
511
  lastActivityTime = Date.now();
513
512
  lastEntryCount += newEntries.length;
514
- console.log(`[monitor] ${task.id}: +${newEntries.length} entries (${lastEntryCount} total)`);
513
+ // Monitor entries tracked in task log only
515
514
 
516
515
  appendLog(task.id, {
517
516
  type: 'system', subtype: 'text',
@@ -105,7 +105,7 @@ async function poll() {
105
105
 
106
106
  async function handleMessage(msg: any) {
107
107
  const chatId = msg.chat.id;
108
- console.log(`[telegram] Message from chat ID: ${chatId}, user: ${msg.from?.username || msg.from?.first_name || 'unknown'}`);
108
+ // Message received (logged silently)
109
109
  const text: string = msg.text.trim();
110
110
  const replyTo = msg.reply_to_message?.message_id;
111
111
 
@@ -215,6 +215,12 @@ wss.on('connection', (ws: WebSocket) => {
215
215
  return;
216
216
  }
217
217
 
218
+ // Kill previous pty process before attaching to new session (prevents PTY leak)
219
+ if (term) {
220
+ try { term.kill(); } catch {}
221
+ term = null;
222
+ }
223
+
218
224
  // Ensure mouse and scrollback are enabled (for old sessions too)
219
225
  try {
220
226
  execSync(`${TMUX} set-option -t ${name} mouse on 2>/dev/null`);
@@ -239,7 +245,7 @@ wss.on('connection', (ws: WebSocket) => {
239
245
  } as Record<string, string>,
240
246
  });
241
247
 
242
- console.log(`[terminal] Attached to tmux session "${name}" (pid: ${term.pid})`);
248
+ // Attached to tmux session (silent)
243
249
  ws.send(JSON.stringify({ type: 'connected', sessionName: name }));
244
250
 
245
251
  term.onData((data: string) => {
@@ -338,7 +344,7 @@ wss.on('connection', (ws: WebSocket) => {
338
344
  // Only kill the pty attach process, NOT the tmux session — it persists
339
345
  if (term) {
340
346
  term.kill();
341
- console.log(`[terminal] Detached from tmux session "${sessionName}"`);
347
+ // Detached from tmux session (silent)
342
348
  }
343
349
 
344
350
  // Untrack this client
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "bin": {
13
13
  "forge": "./cli/mw.ts",
14
+ "forge-server": "./bin/forge-server.mjs",
14
15
  "mw": "./cli/mw.ts"
15
16
  },
16
17
  "keywords": [