@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 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
 
@@ -141,10 +121,10 @@ forge retry <task-id>
141
121
 
142
122
  ## YAML Workflows
143
123
 
144
- Define multi-step flows in `~/.my-workflow/flows/`:
124
+ Define multi-step flows in `~/.forge/flows/`:
145
125
 
146
126
  ```yaml
147
- # ~/.my-workflow/flows/daily-review.yaml
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 `~/.my-workflow/`:
165
+ All config lives in `~/.forge/`:
186
166
 
187
167
  ```
188
- ~/.my-workflow/
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 ~/.my-workflow/flows/*.yaml`);
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(), '.my-workflow', 'password.json');
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@my-workflow' };
24
+ return { id: 'local', name: 'zliu', email: 'local@forge' };
24
25
  }
25
26
  return null;
26
27
  },
@@ -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(), '.my-workflow', 'bin');
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': 'my-workflow' } }, (res) => {
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 ~/.my-workflow/flows/*.yaml
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(), '.my-workflow', 'flows');
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 ~/.my-workflow/password.json with date.
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(), '.my-workflow', 'password.json');
12
+ const PASSWORD_FILE = join(homedir(), '.forge', 'password.json');
13
13
 
14
14
  function generatePassword(): string {
15
15
  // 8-char alphanumeric, easy to type
@@ -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(), '.my-workflow', 'data.db');
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(), '.my-workflow', 'settings.yaml');
6
+ const SETTINGS_FILE = join(homedir(), '.forge', 'settings.yaml');
7
7
 
8
8
  export interface Settings {
9
9
  projectRoots: string[]; // Multiple project directories
@@ -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
 
@@ -39,7 +39,7 @@ delete process.env.CLAUDECODE;
39
39
 
40
40
  // ─── Shared state persistence ─────────────────────────────────
41
41
 
42
- const STATE_DIR = join(homedir(), '.my-workflow');
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
- console.log(`[terminal] Attached to tmux session "${name}" (pid: ${term.pid})`);
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
- console.log(`[terminal] Detached from tmux session "${sessionName}"`);
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/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.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": [
@@ -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(), '.my-workflow');
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