@aion0/forge 0.1.1 → 0.1.3
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 +12 -32
- package/bin/forge-server.mjs +89 -0
- package/cli/mw.ts +2 -2
- package/components/WebTerminal.tsx +1 -1
- package/lib/auth.ts +2 -1
- package/lib/cloudflared.ts +2 -2
- package/lib/flows.ts +2 -2
- package/lib/init.ts +6 -0
- package/lib/password.ts +2 -2
- package/lib/session-watcher.ts +1 -1
- package/lib/settings.ts +1 -1
- package/lib/task-manager.ts +5 -6
- package/lib/telegram-bot.ts +1 -1
- package/lib/terminal-standalone.ts +3 -3
- package/next-env.d.ts +1 -1
- package/package.json +2 -1
- package/src/config/index.ts +1 -1
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
|
|
48
|
+
pnpm dev
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
## Quick Start
|
|
46
52
|
|
|
47
|
-
### 1.
|
|
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
|
|
|
@@ -141,10 +121,10 @@ forge retry <task-id>
|
|
|
141
121
|
|
|
142
122
|
## YAML Workflows
|
|
143
123
|
|
|
144
|
-
Define multi-step flows in `~/.
|
|
124
|
+
Define multi-step flows in `~/.forge/flows/`:
|
|
145
125
|
|
|
146
126
|
```yaml
|
|
147
|
-
# ~/.
|
|
127
|
+
# ~/.forge/flows/daily-review.yaml
|
|
148
128
|
name: daily-review
|
|
149
129
|
steps:
|
|
150
130
|
- project: my-app
|
|
@@ -182,10 +162,10 @@ Password-protected commands auto-delete your message to keep credentials safe.
|
|
|
182
162
|
|
|
183
163
|
## Configuration
|
|
184
164
|
|
|
185
|
-
All config lives in `~/.
|
|
165
|
+
All config lives in `~/.forge/`:
|
|
186
166
|
|
|
187
167
|
```
|
|
188
|
-
~/.
|
|
168
|
+
~/.forge/
|
|
189
169
|
settings.yaml # Main configuration
|
|
190
170
|
password.json # Daily auto-generated login password
|
|
191
171
|
data.db # SQLite database (tasks, sessions)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* forge-server — Start the Forge web platform.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* forge-server Start in foreground (production mode)
|
|
7
|
+
* forge-server --dev Start in foreground (development mode)
|
|
8
|
+
* forge-server --background Start in background (production mode), logs to ~/.forge/forge.log
|
|
9
|
+
* forge-server --stop Stop background server
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync, spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, openSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = join(__dirname, '..');
|
|
20
|
+
const DATA_DIR = join(homedir(), '.forge');
|
|
21
|
+
const PID_FILE = join(DATA_DIR, 'forge.pid');
|
|
22
|
+
const LOG_FILE = join(DATA_DIR, 'forge.log');
|
|
23
|
+
|
|
24
|
+
const isDev = process.argv.includes('--dev');
|
|
25
|
+
const isBackground = process.argv.includes('--background');
|
|
26
|
+
const isStop = process.argv.includes('--stop');
|
|
27
|
+
|
|
28
|
+
process.chdir(ROOT);
|
|
29
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// ── Stop ──
|
|
32
|
+
if (isStop) {
|
|
33
|
+
try {
|
|
34
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim());
|
|
35
|
+
process.kill(pid, 'SIGTERM');
|
|
36
|
+
unlinkSync(PID_FILE);
|
|
37
|
+
console.log(`[forge] Stopped (pid ${pid})`);
|
|
38
|
+
} catch {
|
|
39
|
+
console.log('[forge] No running server found');
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Background ──
|
|
45
|
+
if (isBackground) {
|
|
46
|
+
// Build if needed
|
|
47
|
+
if (!existsSync(join(ROOT, '.next'))) {
|
|
48
|
+
console.log('[forge] Building...');
|
|
49
|
+
execSync('npx next build', { cwd: ROOT, stdio: 'inherit' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const logFd = openSync(LOG_FILE, 'a');
|
|
53
|
+
const child = spawn('npx', ['next', 'start'], {
|
|
54
|
+
cwd: ROOT,
|
|
55
|
+
stdio: ['ignore', logFd, logFd],
|
|
56
|
+
env: { ...process.env },
|
|
57
|
+
detached: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
61
|
+
child.unref();
|
|
62
|
+
console.log(`[forge] Started in background (pid ${child.pid})`);
|
|
63
|
+
console.log(`[forge] Log: ${LOG_FILE}`);
|
|
64
|
+
console.log(`[forge] Stop: forge-server --stop`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Foreground ──
|
|
69
|
+
if (isDev) {
|
|
70
|
+
console.log('[forge] Starting in development mode...');
|
|
71
|
+
const child = spawn('npx', ['next', 'dev', '--turbopack'], {
|
|
72
|
+
cwd: ROOT,
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
env: { ...process.env },
|
|
75
|
+
});
|
|
76
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
77
|
+
} else {
|
|
78
|
+
if (!existsSync(join(ROOT, '.next'))) {
|
|
79
|
+
console.log('[forge] Building...');
|
|
80
|
+
execSync('npx next build', { cwd: ROOT, stdio: 'inherit' });
|
|
81
|
+
}
|
|
82
|
+
console.log('[forge] Starting server...');
|
|
83
|
+
const child = spawn('npx', ['next', 'start'], {
|
|
84
|
+
cwd: ROOT,
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
env: { ...process.env },
|
|
87
|
+
});
|
|
88
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
89
|
+
}
|
package/cli/mw.ts
CHANGED
|
@@ -229,7 +229,7 @@ async function main() {
|
|
|
229
229
|
const flows = await api('/api/flows');
|
|
230
230
|
if (flows.length === 0) {
|
|
231
231
|
console.log('No flows defined.');
|
|
232
|
-
console.log(`Create flows in ~/.
|
|
232
|
+
console.log(`Create flows in ~/.forge/flows/*.yaml`);
|
|
233
233
|
break;
|
|
234
234
|
}
|
|
235
235
|
for (const f of flows) {
|
|
@@ -313,7 +313,7 @@ async function main() {
|
|
|
313
313
|
const { readFileSync } = await import('node:fs');
|
|
314
314
|
const { homedir } = await import('node:os');
|
|
315
315
|
const { join } = await import('node:path');
|
|
316
|
-
const pwFile = join(homedir(), '.
|
|
316
|
+
const pwFile = join(homedir(), '.forge', 'password.json');
|
|
317
317
|
try {
|
|
318
318
|
const data = JSON.parse(readFileSync(pwFile, 'utf-8'));
|
|
319
319
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -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',
|
package/lib/auth.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Google from 'next-auth/providers/google';
|
|
|
3
3
|
import Credentials from 'next-auth/providers/credentials';
|
|
4
4
|
|
|
5
5
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
6
|
+
trustHost: true,
|
|
6
7
|
providers: [
|
|
7
8
|
// Google OAuth — for production use
|
|
8
9
|
Google({
|
|
@@ -20,7 +21,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
|
20
21
|
const { getPassword } = await import('./password');
|
|
21
22
|
const localPassword = getPassword();
|
|
22
23
|
if (credentials?.password === localPassword) {
|
|
23
|
-
return { id: 'local', name: 'zliu', email: 'local@
|
|
24
|
+
return { id: 'local', name: 'zliu', email: 'local@forge' };
|
|
24
25
|
}
|
|
25
26
|
return null;
|
|
26
27
|
},
|
package/lib/cloudflared.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|
|
10
10
|
import https from 'node:https';
|
|
11
11
|
import http from 'node:http';
|
|
12
12
|
|
|
13
|
-
const BIN_DIR = join(homedir(), '.
|
|
13
|
+
const BIN_DIR = join(homedir(), '.forge', 'bin');
|
|
14
14
|
const BIN_NAME = platform() === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
15
15
|
const BIN_PATH = join(BIN_DIR, BIN_NAME);
|
|
16
16
|
|
|
@@ -40,7 +40,7 @@ function getDownloadUrl(): string {
|
|
|
40
40
|
function followRedirects(url: string, dest: string): Promise<void> {
|
|
41
41
|
return new Promise((resolve, reject) => {
|
|
42
42
|
const client = url.startsWith('https') ? https : http;
|
|
43
|
-
client.get(url, { headers: { 'User-Agent': '
|
|
43
|
+
client.get(url, { headers: { 'User-Agent': 'forge' } }, (res) => {
|
|
44
44
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
45
45
|
followRedirects(res.headers.location, dest).then(resolve, reject);
|
|
46
46
|
return;
|
package/lib/flows.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow (Flow) engine — loads YAML flow definitions and executes them.
|
|
3
3
|
*
|
|
4
|
-
* Flow files live in ~/.
|
|
4
|
+
* Flow files live in ~/.forge/flows/*.yaml
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
@@ -12,7 +12,7 @@ import { createTask } from './task-manager';
|
|
|
12
12
|
import { getProjectInfo } from './projects';
|
|
13
13
|
import type { Task } from '@/src/types';
|
|
14
14
|
|
|
15
|
-
const FLOWS_DIR = join(homedir(), '.
|
|
15
|
+
const FLOWS_DIR = join(homedir(), '.forge', 'flows');
|
|
16
16
|
|
|
17
17
|
export interface FlowStep {
|
|
18
18
|
project: string;
|
package/lib/init.ts
CHANGED
|
@@ -19,6 +19,12 @@ export function ensureInitialized() {
|
|
|
19
19
|
if (gInit[initKey]) return;
|
|
20
20
|
gInit[initKey] = true;
|
|
21
21
|
|
|
22
|
+
// Ensure AUTH_SECRET is set (NextAuth requires it)
|
|
23
|
+
if (!process.env.AUTH_SECRET) {
|
|
24
|
+
const { randomBytes } = require('node:crypto');
|
|
25
|
+
process.env.AUTH_SECRET = randomBytes(32).toString('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
// Display login password (auto-generated, rotates daily)
|
|
23
29
|
const password = getPassword();
|
|
24
30
|
console.log(`[init] Login password: ${password} (valid today)`);
|
package/lib/password.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-generated login password.
|
|
3
|
-
* Rotates daily. Saved to ~/.
|
|
3
|
+
* Rotates daily. Saved to ~/.forge/password.json with date.
|
|
4
4
|
* CLI can read it via `mw password`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ import { homedir } from 'node:os';
|
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
|
|
12
|
-
const PASSWORD_FILE = join(homedir(), '.
|
|
12
|
+
const PASSWORD_FILE = join(homedir(), '.forge', 'password.json');
|
|
13
13
|
|
|
14
14
|
function generatePassword(): string {
|
|
15
15
|
// 8-char alphanumeric, easy to type
|
package/lib/session-watcher.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { scanProjects } from './projects';
|
|
20
20
|
import { loadSettings } from './settings';
|
|
21
21
|
|
|
22
|
-
const DB_PATH = join(homedir(), '.
|
|
22
|
+
const DB_PATH = join(homedir(), '.forge', 'data.db');
|
|
23
23
|
|
|
24
24
|
// ─── Types ───────────────────────────────────────────────────
|
|
25
25
|
|
package/lib/settings.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
|
-
const SETTINGS_FILE = join(homedir(), '.
|
|
6
|
+
const SETTINGS_FILE = join(homedir(), '.forge', 'settings.yaml');
|
|
7
7
|
|
|
8
8
|
export interface Settings {
|
|
9
9
|
projectRoots: string[]; // Multiple project directories
|
package/lib/task-manager.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
513
|
+
// Monitor entries tracked in task log only
|
|
515
514
|
|
|
516
515
|
appendLog(task.id, {
|
|
517
516
|
type: 'system', subtype: 'text',
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -105,7 +105,7 @@ async function poll() {
|
|
|
105
105
|
|
|
106
106
|
async function handleMessage(msg: any) {
|
|
107
107
|
const chatId = msg.chat.id;
|
|
108
|
-
|
|
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
|
|
|
@@ -39,7 +39,7 @@ delete process.env.CLAUDECODE;
|
|
|
39
39
|
|
|
40
40
|
// ─── Shared state persistence ─────────────────────────────────
|
|
41
41
|
|
|
42
|
-
const STATE_DIR = join(homedir(), '.
|
|
42
|
+
const STATE_DIR = join(homedir(), '.forge');
|
|
43
43
|
const STATE_FILE = join(STATE_DIR, 'terminal-state.json');
|
|
44
44
|
|
|
45
45
|
function loadTerminalState(): unknown {
|
|
@@ -245,7 +245,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
245
245
|
} as Record<string, string>,
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// Attached to tmux session (silent)
|
|
249
249
|
ws.send(JSON.stringify({ type: 'connected', sessionName: name }));
|
|
250
250
|
|
|
251
251
|
term.onData((data: string) => {
|
|
@@ -344,7 +344,7 @@ wss.on('connection', (ws: WebSocket) => {
|
|
|
344
344
|
// Only kill the pty attach process, NOT the tmux session — it persists
|
|
345
345
|
if (term) {
|
|
346
346
|
term.kill();
|
|
347
|
-
|
|
347
|
+
// Detached from tmux session (silent)
|
|
348
348
|
}
|
|
349
349
|
|
|
350
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/
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
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": [
|
package/src/config/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
import type { AppConfig, ProviderName, SessionTemplate } from '@/src/types';
|
|
6
6
|
|
|
7
|
-
const CONFIG_DIR = join(homedir(), '.
|
|
7
|
+
const CONFIG_DIR = join(homedir(), '.forge');
|
|
8
8
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
|
|
9
9
|
const TEMPLATES_DIR = join(CONFIG_DIR, 'templates');
|
|
10
10
|
|