@aion0/forge 0.2.8 → 0.2.10

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.
@@ -0,0 +1,58 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { execSync } from 'node:child_process';
3
+
4
+ function run(cmd: string): string {
5
+ try {
6
+ return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
7
+ } catch { return ''; }
8
+ }
9
+
10
+ function countProcess(pattern: string): { count: number; pid: string } {
11
+ const out = run(`ps aux | grep '${pattern}' | grep -v grep | head -1`);
12
+ const pid = out ? out.split(/\s+/)[1] || '' : '';
13
+ const count = out ? run(`ps aux | grep '${pattern}' | grep -v grep | wc -l`).trim() : '0';
14
+ return { count: parseInt(count), pid };
15
+ }
16
+
17
+ export async function GET() {
18
+ // Processes
19
+ const nextjs = countProcess('next-server');
20
+ const terminal = countProcess('terminal-standalone');
21
+ const telegram = countProcess('telegram-standalone');
22
+ const tunnel = countProcess('cloudflared tunnel');
23
+
24
+ // Tunnel URL
25
+ let tunnelUrl = '';
26
+ try {
27
+ const { readFileSync } = require('fs');
28
+ const { join } = require('path');
29
+ const { homedir } = require('os');
30
+ const state = JSON.parse(readFileSync(join(homedir(), '.forge', 'tunnel-state.json'), 'utf-8'));
31
+ tunnelUrl = state.url || '';
32
+ } catch {}
33
+
34
+ // tmux sessions
35
+ let sessions: { name: string; created: string; attached: boolean; windows: number }[] = [];
36
+ try {
37
+ const out = run("tmux list-sessions -F '#{session_name}||#{session_created}||#{session_attached}||#{session_windows}' 2>/dev/null");
38
+ sessions = out.split('\n').filter(l => l.startsWith('mw-')).map(line => {
39
+ const [name, created, attached, windows] = line.split('||');
40
+ return { name, created: new Date(Number(created) * 1000).toISOString(), attached: attached !== '0', windows: Number(windows) || 1 };
41
+ });
42
+ } catch {}
43
+
44
+ // System info
45
+ const uptime = run('uptime');
46
+ const memory = run("ps -o rss= -p $$ 2>/dev/null || echo 0");
47
+
48
+ return NextResponse.json({
49
+ processes: {
50
+ nextjs: { running: nextjs.count > 0, pid: nextjs.pid },
51
+ terminal: { running: terminal.count > 0, pid: terminal.pid },
52
+ telegram: { running: telegram.count > 0, pid: telegram.pid },
53
+ tunnel: { running: tunnel.count > 0, pid: tunnel.pid, url: tunnelUrl },
54
+ },
55
+ sessions,
56
+ uptime: uptime.replace(/.*up\s+/, '').replace(/,\s+\d+ user.*/, '').trim(),
57
+ });
58
+ }
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # check-forge-status.sh — Show Forge process status
3
+
4
+ echo "══════════════════════════════════"
5
+ echo " Forge Process Status"
6
+ echo "══════════════════════════════════"
7
+
8
+ # Next.js
9
+ count=$(ps aux | grep 'next-server' | grep -v grep | wc -l | tr -d ' ')
10
+ pid=$(ps aux | grep 'next-server' | grep -v grep | awk '{print $2}' | head -1)
11
+ if [ "$count" -gt 0 ]; then
12
+ echo " ● Next.js running (pid: $pid)"
13
+ else
14
+ echo " ○ Next.js stopped"
15
+ fi
16
+
17
+ # Terminal
18
+ count=$(ps aux | grep 'terminal-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
19
+ pid=$(ps aux | grep 'terminal-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
20
+ if [ "$count" -gt 0 ]; then
21
+ echo " ● Terminal running (pid: $pid)"
22
+ else
23
+ echo " ○ Terminal stopped"
24
+ fi
25
+
26
+ # Telegram
27
+ count=$(ps aux | grep 'telegram-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
28
+ pid=$(ps aux | grep 'telegram-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
29
+ if [ "$count" -gt 0 ]; then
30
+ echo " ● Telegram running (pid: $pid)"
31
+ else
32
+ echo " ○ Telegram stopped"
33
+ fi
34
+
35
+ # Cloudflare Tunnel
36
+ count=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | wc -l | tr -d ' ')
37
+ pid=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | awk '{print $2}' | head -1)
38
+ url=$(cat ~/.forge/tunnel-state.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('url',''))" 2>/dev/null)
39
+ if [ "$count" -gt 0 ]; then
40
+ echo " ● Tunnel running (pid: $pid) ${url}"
41
+ else
42
+ echo " ○ Tunnel stopped"
43
+ fi
44
+
45
+ # tmux sessions
46
+ tmux_count=$(tmux list-sessions 2>/dev/null | grep '^mw-' | wc -l | tr -d ' ')
47
+ echo ""
48
+ echo " Terminal sessions: $tmux_count"
49
+ tmux list-sessions 2>/dev/null | grep '^mw-' | while read line; do
50
+ echo " $line"
51
+ done
52
+
53
+ echo "══════════════════════════════════"
@@ -8,6 +8,7 @@ import SessionView from './SessionView';
8
8
  import NewTaskModal from './NewTaskModal';
9
9
  import SettingsModal from './SettingsModal';
10
10
  import TunnelToggle from './TunnelToggle';
11
+ import MonitorPanel from './MonitorPanel';
11
12
  import type { Task } from '@/src/types';
12
13
  import type { WebTerminalHandle } from './WebTerminal';
13
14
 
@@ -44,6 +45,7 @@ export default function Dashboard({ user }: { user: any }) {
44
45
  const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
45
46
  const [showNewTask, setShowNewTask] = useState(false);
46
47
  const [showSettings, setShowSettings] = useState(false);
48
+ const [showMonitor, setShowMonitor] = useState(false);
47
49
  const [usage, setUsage] = useState<UsageSummary[]>([]);
48
50
  const [providers, setProviders] = useState<ProviderInfo[]>([]);
49
51
  const [projects, setProjects] = useState<ProjectInfo[]>([]);
@@ -194,6 +196,12 @@ export default function Dashboard({ user }: { user: any }) {
194
196
  )}
195
197
  </span>
196
198
  )}
199
+ <button
200
+ onClick={() => setShowMonitor(true)}
201
+ className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
202
+ >
203
+ Monitor
204
+ </button>
197
205
  <button
198
206
  onClick={() => setShowSettings(true)}
199
207
  className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
@@ -365,6 +373,8 @@ export default function Dashboard({ user }: { user: any }) {
365
373
  />
366
374
  )}
367
375
 
376
+ {showMonitor && <MonitorPanel onClose={() => setShowMonitor(false)} />}
377
+
368
378
  {showSettings && (
369
379
  <SettingsModal onClose={() => { setShowSettings(false); fetchData(); }} />
370
380
  )}
@@ -22,6 +22,13 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
22
22
  const [connected, setConnected] = useState(false);
23
23
  const wsRef = useRef<WebSocket | null>(null);
24
24
  const docRootRef = useRef(docRoot);
25
+ const skipPermRef = useRef(false);
26
+
27
+ useEffect(() => {
28
+ fetch('/api/settings').then(r => r.json())
29
+ .then((s: any) => { if (s.skipPermissions) skipPermRef.current = true; })
30
+ .catch(() => {});
31
+ }, []);
25
32
  docRootRef.current = docRoot;
26
33
 
27
34
  useEffect(() => {
@@ -78,7 +85,8 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
78
85
  isNewSession = false;
79
86
  setTimeout(() => {
80
87
  if (socket.readyState === WebSocket.OPEN) {
81
- socket.send(JSON.stringify({ type: 'input', data: `cd "${docRootRef.current}" && claude --resume\n` }));
88
+ const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
89
+ socket.send(JSON.stringify({ type: 'input', data: `cd "${docRootRef.current}" && claude --resume${sf}\n` }));
82
90
  }
83
91
  }, 300);
84
92
  }
@@ -147,13 +155,13 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
147
155
  </span>
148
156
  <div className="ml-auto flex items-center gap-1">
149
157
  <button
150
- onClick={() => runCommand(`cd "${docRoot}" && claude`)}
158
+ onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude${sf}`); }}
151
159
  className="text-[10px] px-2 py-0.5 text-[var(--accent)] hover:bg-[#2a2a4a] rounded"
152
160
  >
153
161
  New
154
162
  </button>
155
163
  <button
156
- onClick={() => runCommand(`cd "${docRoot}" && claude --resume`)}
164
+ onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude --resume${sf}`); }}
157
165
  className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[#2a2a4a] rounded"
158
166
  >
159
167
  Resume
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+
5
+ interface MonitorData {
6
+ processes: {
7
+ nextjs: { running: boolean; pid: string };
8
+ terminal: { running: boolean; pid: string };
9
+ telegram: { running: boolean; pid: string };
10
+ tunnel: { running: boolean; pid: string; url: string };
11
+ };
12
+ sessions: { name: string; created: string; attached: boolean; windows: number }[];
13
+ uptime: string;
14
+ }
15
+
16
+ export default function MonitorPanel({ onClose }: { onClose: () => void }) {
17
+ const [data, setData] = useState<MonitorData | null>(null);
18
+
19
+ const refresh = useCallback(() => {
20
+ fetch('/api/monitor').then(r => r.json()).then(setData).catch(() => {});
21
+ }, []);
22
+
23
+ useEffect(() => {
24
+ refresh();
25
+ const timer = setInterval(refresh, 5000);
26
+ return () => clearInterval(timer);
27
+ }, [refresh]);
28
+
29
+ return (
30
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
31
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[500px] max-h-[80vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
32
+ <div className="px-4 py-3 border-b border-[var(--border)] flex items-center justify-between">
33
+ <h2 className="text-sm font-bold text-[var(--text-primary)]">Monitor</h2>
34
+ <div className="flex items-center gap-2">
35
+ <button onClick={refresh} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">↻</button>
36
+ <button onClick={onClose} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">Close</button>
37
+ </div>
38
+ </div>
39
+
40
+ {data ? (
41
+ <div className="p-4 space-y-4">
42
+ {/* Processes */}
43
+ <div>
44
+ <h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-2">Processes</h3>
45
+ <div className="space-y-1.5">
46
+ {[
47
+ { label: 'Next.js', ...data.processes.nextjs },
48
+ { label: 'Terminal Server', ...data.processes.terminal },
49
+ { label: 'Telegram Bot', ...data.processes.telegram },
50
+ { label: 'Tunnel', ...data.processes.tunnel },
51
+ ].map(p => (
52
+ <div key={p.label} className="flex items-center gap-2 text-xs">
53
+ <span className={p.running ? 'text-green-400' : 'text-gray-500'}>●</span>
54
+ <span className="text-[var(--text-primary)] w-28">{p.label}</span>
55
+ {p.running ? (
56
+ <span className="text-[var(--text-secondary)] font-mono text-[10px]">pid: {p.pid}</span>
57
+ ) : (
58
+ <span className="text-gray-500 text-[10px]">stopped</span>
59
+ )}
60
+ </div>
61
+ ))}
62
+ {data.processes.tunnel.running && data.processes.tunnel.url && (
63
+ <div className="pl-6 text-[10px] text-[var(--accent)] truncate">{data.processes.tunnel.url}</div>
64
+ )}
65
+ </div>
66
+ </div>
67
+
68
+ {/* Uptime */}
69
+ {data.uptime && (
70
+ <div className="text-[10px] text-[var(--text-secondary)]">
71
+ Uptime: {data.uptime}
72
+ </div>
73
+ )}
74
+
75
+ {/* Sessions */}
76
+ <div>
77
+ <h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-2">
78
+ Terminal Sessions ({data.sessions.length})
79
+ </h3>
80
+ {data.sessions.length === 0 ? (
81
+ <div className="text-[10px] text-[var(--text-secondary)]">No sessions</div>
82
+ ) : (
83
+ <div className="space-y-1">
84
+ {data.sessions.map(s => (
85
+ <div key={s.name} className="flex items-center gap-2 text-[11px]">
86
+ <span className={s.attached ? 'text-green-400' : 'text-yellow-500'}>●</span>
87
+ <span className="font-mono text-[var(--text-primary)] truncate flex-1">{s.name}</span>
88
+ <span className="text-[9px] text-[var(--text-secondary)]">{s.attached ? 'attached' : 'detached'}</span>
89
+ <span className="text-[9px] text-[var(--text-secondary)]">{new Date(s.created).toLocaleTimeString()}</span>
90
+ </div>
91
+ ))}
92
+ </div>
93
+ )}
94
+ </div>
95
+ </div>
96
+ ) : (
97
+ <div className="p-8 text-center text-xs text-[var(--text-secondary)]">Loading...</div>
98
+ )}
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -42,6 +42,7 @@ interface Settings {
42
42
  taskModel: string;
43
43
  pipelineModel: string;
44
44
  telegramModel: string;
45
+ skipPermissions: boolean;
45
46
  }
46
47
 
47
48
  interface TunnelStatus {
@@ -66,6 +67,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
66
67
  taskModel: 'sonnet',
67
68
  pipelineModel: 'sonnet',
68
69
  telegramModel: 'sonnet',
70
+ skipPermissions: false,
69
71
  });
70
72
  const [newRoot, setNewRoot] = useState('');
71
73
  const [newDocRoot, setNewDocRoot] = useState('');
@@ -347,6 +349,22 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
347
349
  </div>
348
350
  </div>
349
351
 
352
+ {/* Permissions */}
353
+ <div className="space-y-2">
354
+ <label className="flex items-center gap-2 text-xs text-[var(--text-primary)] cursor-pointer">
355
+ <input
356
+ type="checkbox"
357
+ checked={settings.skipPermissions || false}
358
+ onChange={e => setSettings({ ...settings, skipPermissions: e.target.checked })}
359
+ className="rounded"
360
+ />
361
+ Skip permissions check (--dangerously-skip-permissions)
362
+ </label>
363
+ <p className="text-[9px] text-[var(--text-secondary)]">
364
+ When enabled, all Claude Code tasks and pipelines run without permission prompts. Useful for background automation but less safe.
365
+ </p>
366
+ </div>
367
+
350
368
  {/* Remote Access (Cloudflare Tunnel) */}
351
369
  <div className="space-y-2">
352
370
  <label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
@@ -182,6 +182,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
182
182
  const [showNewTabModal, setShowNewTabModal] = useState(false);
183
183
  const [projectRoots, setProjectRoots] = useState<string[]>([]);
184
184
  const [allProjects, setAllProjects] = useState<{ name: string; path: string; root: string }[]>([]);
185
+ const [skipPermissions, setSkipPermissions] = useState(false);
185
186
  const [expandedRoot, setExpandedRoot] = useState<string | null>(null);
186
187
 
187
188
  // Restore shared state from server after mount
@@ -196,6 +197,10 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
196
197
  }
197
198
  setHydrated(true);
198
199
  });
200
+ // Fetch settings for skipPermissions
201
+ fetch('/api/settings').then(r => r.json())
202
+ .then((s: any) => { if (s.skipPermissions) setSkipPermissions(true); })
203
+ .catch(() => {});
199
204
  // Fetch projects and derive roots
200
205
  fetch('/api/projects').then(r => r.json())
201
206
  .then((p: { name: string; path: string; root: string }[]) => {
@@ -765,6 +770,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
765
770
  setRatios={tab.id === activeTabId ? setRatios : () => {}}
766
771
  onSessionConnected={onSessionConnected}
767
772
  refreshKeys={refreshKeys}
773
+ skipPermissions={skipPermissions}
768
774
  />
769
775
  </div>
770
776
  ))}
@@ -777,7 +783,7 @@ export default WebTerminal;
777
783
  // ─── Pane renderer ───────────────────────────────────────────
778
784
 
779
785
  function PaneRenderer({
780
- node, activeId, onFocus, ratios, setRatios, onSessionConnected, refreshKeys,
786
+ node, activeId, onFocus, ratios, setRatios, onSessionConnected, refreshKeys, skipPermissions,
781
787
  }: {
782
788
  node: SplitNode;
783
789
  activeId: number;
@@ -786,11 +792,12 @@ function PaneRenderer({
786
792
  setRatios: React.Dispatch<React.SetStateAction<Record<number, number>>>;
787
793
  onSessionConnected: (paneId: number, sessionName: string) => void;
788
794
  refreshKeys: Record<number, number>;
795
+ skipPermissions?: boolean;
789
796
  }) {
790
797
  if (node.type === 'terminal') {
791
798
  return (
792
799
  <div className={`h-full w-full ${activeId === node.id ? 'ring-1 ring-[#7c5bf0]/50 ring-inset' : ''}`} onMouseDown={() => onFocus(node.id)}>
793
- <MemoTerminalPane key={`${node.id}-${refreshKeys[node.id] || 0}`} id={node.id} sessionName={node.sessionName} projectPath={node.projectPath} onSessionConnected={onSessionConnected} />
800
+ <MemoTerminalPane key={`${node.id}-${refreshKeys[node.id] || 0}`} id={node.id} sessionName={node.sessionName} projectPath={node.projectPath} skipPermissions={skipPermissions} onSessionConnected={onSessionConnected} />
794
801
  </div>
795
802
  );
796
803
  }
@@ -799,8 +806,8 @@ function PaneRenderer({
799
806
 
800
807
  return (
801
808
  <DraggableSplit splitId={node.id} direction={node.direction} ratio={ratio} setRatios={setRatios}>
802
- <PaneRenderer node={node.first} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} />
803
- <PaneRenderer node={node.second} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} />
809
+ <PaneRenderer node={node.first} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} skipPermissions={skipPermissions} />
810
+ <PaneRenderer node={node.second} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} skipPermissions={skipPermissions} />
804
811
  </DraggableSplit>
805
812
  );
806
813
  }
@@ -921,17 +928,21 @@ const MemoTerminalPane = memo(function TerminalPane({
921
928
  id,
922
929
  sessionName,
923
930
  projectPath,
931
+ skipPermissions,
924
932
  onSessionConnected,
925
933
  }: {
926
934
  id: number;
927
935
  sessionName?: string;
928
936
  projectPath?: string;
937
+ skipPermissions?: boolean;
929
938
  onSessionConnected: (paneId: number, sessionName: string) => void;
930
939
  }) {
931
940
  const containerRef = useRef<HTMLDivElement>(null);
932
941
  const sessionNameRef = useRef(sessionName);
933
942
  sessionNameRef.current = sessionName;
934
943
  const projectPathRef = useRef(projectPath);
944
+ const skipPermRef = useRef(skipPermissions);
945
+ skipPermRef.current = skipPermissions;
935
946
  projectPathRef.current = projectPath;
936
947
 
937
948
  useEffect(() => {
@@ -1055,7 +1066,8 @@ const MemoTerminalPane = memo(function TerminalPane({
1055
1066
  isNewlyCreated = false;
1056
1067
  setTimeout(() => {
1057
1068
  if (!disposed && ws?.readyState === WebSocket.OPEN) {
1058
- ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude --resume\n` }));
1069
+ const skipFlag = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
1070
+ ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude --resume${skipFlag}\n` }));
1059
1071
  }
1060
1072
  }, 300);
1061
1073
  }
package/lib/settings.ts CHANGED
@@ -19,6 +19,7 @@ export interface Settings {
19
19
  taskModel: string; // Model for tasks (default: sonnet)
20
20
  pipelineModel: string; // Model for pipelines (default: sonnet)
21
21
  telegramModel: string; // Model for Telegram AI features (default: sonnet)
22
+ skipPermissions: boolean; // Add --dangerously-skip-permissions to all claude invocations
22
23
  }
23
24
 
24
25
  const defaults: Settings = {
@@ -34,6 +35,7 @@ const defaults: Settings = {
34
35
  taskModel: 'default',
35
36
  pipelineModel: 'default',
36
37
  telegramModel: 'sonnet',
38
+ skipPermissions: false,
37
39
  };
38
40
 
39
41
  export function loadSettings(): Settings {
@@ -16,11 +16,11 @@ import { startTunnel, stopTunnel, getTunnelStatus } from './cloudflared';
16
16
  import { getPassword } from './password';
17
17
  import type { Task, TaskLogEntry } from '@/src/types';
18
18
 
19
- // Prevent duplicate polling and state loss across hot-reloads
19
+ // Persist state across hot-reloads
20
20
  const globalKey = Symbol.for('mw-telegram-state');
21
21
  const g = globalThis as any;
22
- if (!g[globalKey]) g[globalKey] = { polling: false, pollTimer: null, lastUpdateId: 0, taskListenerAttached: false, pollActive: false, processedMsgIds: new Set<number>(), pollGeneration: 0 };
23
- const botState: { polling: boolean; pollTimer: ReturnType<typeof setTimeout> | null; lastUpdateId: number; taskListenerAttached: boolean; pollActive: boolean; processedMsgIds: Set<number>; pollGeneration: number } = g[globalKey];
22
+ if (!g[globalKey]) g[globalKey] = { taskListenerAttached: false, processedMsgIds: new Set<number>() };
23
+ const botState: { taskListenerAttached: boolean; processedMsgIds: Set<number> } = g[globalKey];
24
24
 
25
25
  // Track which Telegram message maps to which task (for reply-based interaction)
26
26
  const taskMessageMap = new Map<number, string>(); // messageId → taskId
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.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {