@aion0/forge 0.1.10 → 0.2.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.
@@ -34,7 +34,10 @@ const chatNumberedTasks = new Map<number, Map<number, string>>();
34
34
  const chatNumberedSessions = new Map<number, Map<number, { projectName: string; sessionId: string }>>();
35
35
  const chatNumberedProjects = new Map<number, Map<number, string>>();
36
36
  // Track what the last numbered list was for
37
- const chatListMode = new Map<number, 'tasks' | 'projects' | 'sessions'>();
37
+ const chatListMode = new Map<number, 'tasks' | 'projects' | 'sessions' | 'task-create' | 'peek'>();
38
+
39
+ // Pending task creation: waiting for prompt text
40
+ const pendingTaskProject = new Map<number, { name: string; path: string }>(); // chatId → project
38
41
 
39
42
  // Buffer for streaming logs
40
43
  const logBuffers = new Map<string, { entries: string[]; timer: ReturnType<typeof setTimeout> | null }>();
@@ -113,6 +116,20 @@ async function handleMessage(msg: any) {
113
116
  const text: string = msg.text.trim();
114
117
  const replyTo = msg.reply_to_message?.message_id;
115
118
 
119
+ // Check if waiting for task prompt
120
+ const pending = pendingTaskProject.get(chatId);
121
+ if (pending && !text.startsWith('/')) {
122
+ pendingTaskProject.delete(chatId);
123
+ const task = createTask({
124
+ projectName: pending.name,
125
+ projectPath: pending.path,
126
+ prompt: text,
127
+ });
128
+ const msgId = await send(chatId, `✅ Task ${task.id} created\n📁 ${task.projectName}\n\n${text.slice(0, 200)}`);
129
+ if (msgId) { taskMessageMap.set(msgId, task.id); taskChatMap.set(task.id, chatId); }
130
+ return;
131
+ }
132
+
116
133
  // Check if replying to a task message → follow-up
117
134
  if (replyTo && taskMessageMap.has(replyTo)) {
118
135
  const taskId = taskMessageMap.get(replyTo)!;
@@ -125,7 +142,25 @@ async function handleMessage(msg: any) {
125
142
  const num = parseInt(text);
126
143
  const mode = chatListMode.get(chatId);
127
144
 
128
- if (mode === 'projects') {
145
+ if (mode === 'task-create') {
146
+ const projMap = chatNumberedProjects.get(chatId);
147
+ if (projMap?.has(num)) {
148
+ const projectName = projMap.get(num)!;
149
+ const projects = scanProjects();
150
+ const project = projects.find(p => p.name === projectName);
151
+ if (project) {
152
+ pendingTaskProject.set(chatId, { name: project.name, path: project.path });
153
+ await send(chatId, `📁 ${project.name}\n\nSend the task prompt:`);
154
+ }
155
+ return;
156
+ }
157
+ } else if (mode === 'peek') {
158
+ const projMap = chatNumberedProjects.get(chatId);
159
+ if (projMap?.has(num)) {
160
+ await handlePeek(chatId, projMap.get(num)!);
161
+ return;
162
+ }
163
+ } else if (mode === 'projects') {
129
164
  const projMap = chatNumberedProjects.get(chatId);
130
165
  if (projMap?.has(num)) {
131
166
  await sendSessionList(chatId, projMap.get(num)!);
@@ -149,6 +184,9 @@ async function handleMessage(msg: any) {
149
184
 
150
185
  // Commands
151
186
  if (text.startsWith('/')) {
187
+ // Any new command cancels pending states
188
+ pendingTaskProject.delete(chatId);
189
+
152
190
  const [cmd, ...args] = text.split(/\s+/);
153
191
  switch (cmd) {
154
192
  case '/start':
@@ -161,7 +199,11 @@ async function handleMessage(msg: any) {
161
199
  break;
162
200
  case '/new':
163
201
  case '/task':
164
- await handleNewTask(chatId, args.join(' '));
202
+ if (args.length > 0) {
203
+ await handleNewTask(chatId, args.join(' '));
204
+ } else {
205
+ await startTaskCreation(chatId);
206
+ }
165
207
  break;
166
208
  case '/sessions':
167
209
  case '/s':
@@ -185,6 +227,17 @@ async function handleMessage(msg: any) {
185
227
  case '/unwatch':
186
228
  await handleUnwatch(chatId, args[0]);
187
229
  break;
230
+ case '/peek':
231
+ if (args.length > 0) {
232
+ await handlePeek(chatId, args[0], args[1]);
233
+ } else {
234
+ await startPeekSelection(chatId);
235
+ }
236
+ break;
237
+ case '/docs':
238
+ case '/doc':
239
+ await handleDocs(chatId, args.join(' '));
240
+ break;
188
241
  case '/cancel':
189
242
  await handleCancel(chatId, args[0]);
190
243
  break;
@@ -238,6 +291,9 @@ async function sendHelp(chatId: number) {
238
291
  `/watchers — list active watchers\n` +
239
292
  `/unwatch <id> — stop watching\n\n` +
240
293
  `📝 Submit task:\nproject-name: your instructions\n\n` +
294
+ `👀 /peek [project] [sessionId] — session summary\n` +
295
+ `📖 /docs — docs session summary\n` +
296
+ `/docs <filename> — view doc file\n\n` +
241
297
  `🔧 /cancel <id> /retry <id>\n` +
242
298
  `/projects — list projects\n\n` +
243
299
  `🌐 /tunnel — tunnel status\n` +
@@ -437,6 +493,167 @@ async function sendSessionContent(chatId: number, projectName: string, sessionId
437
493
  }
438
494
  }
439
495
 
496
+ async function startPeekSelection(chatId: number) {
497
+ const projects = scanProjects();
498
+ if (projects.length === 0) {
499
+ await send(chatId, 'No projects configured.');
500
+ return;
501
+ }
502
+
503
+ // Filter to projects that have sessions
504
+ const withSessions = projects.filter(p => listClaudeSessions(p.name).length > 0);
505
+ if (withSessions.length === 0) {
506
+ await send(chatId, 'No projects with sessions found.');
507
+ return;
508
+ }
509
+
510
+ const numbered = new Map<number, string>();
511
+ const lines = withSessions.slice(0, 15).map((p, i) => {
512
+ numbered.set(i + 1, p.name);
513
+ const sessions = listClaudeSessions(p.name);
514
+ const latest = sessions[0];
515
+ const info = latest?.summary || latest?.firstPrompt?.slice(0, 40) || '';
516
+ return `${i + 1}. ${p.name}${info ? `\n ${info}` : ''}`;
517
+ });
518
+
519
+ chatNumberedProjects.set(chatId, numbered);
520
+ chatListMode.set(chatId, 'peek');
521
+
522
+ await send(chatId, `👀 Peek — select project:\n\n${lines.join('\n')}`);
523
+ }
524
+
525
+ async function handlePeek(chatId: number, projectArg?: string, sessionArg?: string) {
526
+ const projects = scanProjects();
527
+
528
+ // If no project specified, use the most recent task's project
529
+ let projectName = projectArg;
530
+ let sessionId = sessionArg;
531
+
532
+ if (!projectName) {
533
+ // Find most recent running or done task
534
+ const tasks = listTasks();
535
+ const recent = tasks.find(t => t.status === 'running') || tasks[0];
536
+ if (recent) {
537
+ projectName = recent.projectName;
538
+ } else {
539
+ await send(chatId, 'No project specified and no recent tasks.\nUsage: /peek [project] [sessionId]');
540
+ return;
541
+ }
542
+ }
543
+
544
+ const project = projects.find(p => p.name === projectName || p.name.toLowerCase() === projectName!.toLowerCase());
545
+ if (!project) {
546
+ await send(chatId, `Project not found: ${projectName}`);
547
+ return;
548
+ }
549
+
550
+ // Find session
551
+ const sessions = listClaudeSessions(project.name);
552
+ if (sessions.length === 0) {
553
+ await send(chatId, `No sessions for ${project.name}`);
554
+ return;
555
+ }
556
+
557
+ const session = sessionId
558
+ ? sessions.find(s => s.sessionId.startsWith(sessionId!))
559
+ : sessions[0]; // most recent
560
+
561
+ if (!session) {
562
+ await send(chatId, `Session not found: ${sessionId}`);
563
+ return;
564
+ }
565
+
566
+ const filePath = getSessionFilePath(project.name, session.sessionId);
567
+ if (!filePath) {
568
+ await send(chatId, 'Session file not found');
569
+ return;
570
+ }
571
+
572
+ await send(chatId, `🔍 Loading ${project.name} / ${session.sessionId.slice(0, 8)}...`);
573
+
574
+ const entries = readSessionEntries(filePath);
575
+ if (entries.length === 0) {
576
+ await send(chatId, 'Session is empty');
577
+ return;
578
+ }
579
+
580
+ // Collect last N meaningful entries for raw display
581
+ const recentRaw: string[] = [];
582
+ let rawCount = 0;
583
+ for (let i = entries.length - 1; i >= 0 && rawCount < 8; i--) {
584
+ const e = entries[i];
585
+ if (e.type === 'user') {
586
+ recentRaw.unshift(`👤 ${e.content.slice(0, 300)}`);
587
+ rawCount++;
588
+ } else if (e.type === 'assistant_text') {
589
+ recentRaw.unshift(`🤖 ${e.content.slice(0, 300)}`);
590
+ rawCount++;
591
+ } else if (e.type === 'tool_use') {
592
+ recentRaw.unshift(`🔧 ${e.toolName || 'tool'}`);
593
+ rawCount++;
594
+ }
595
+ }
596
+
597
+ // Build context for AI summary (last ~50 entries)
598
+ const contextEntries: string[] = [];
599
+ let contextLen = 0;
600
+ const MAX_CONTEXT = 8000;
601
+ for (let i = entries.length - 1; i >= 0 && contextLen < MAX_CONTEXT; i--) {
602
+ const e = entries[i];
603
+ let line = '';
604
+ if (e.type === 'user') line = `User: ${e.content}`;
605
+ else if (e.type === 'assistant_text') line = `Assistant: ${e.content}`;
606
+ else if (e.type === 'tool_use') line = `Tool: ${e.toolName || 'tool'}`;
607
+ else continue;
608
+ if (contextLen + line.length > MAX_CONTEXT) break;
609
+ contextEntries.unshift(line);
610
+ contextLen += line.length;
611
+ }
612
+
613
+ // AI summary
614
+ let summary = '';
615
+ try {
616
+ const apiKey = process.env.ANTHROPIC_API_KEY;
617
+ if (apiKey) {
618
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
619
+ method: 'POST',
620
+ headers: {
621
+ 'Content-Type': 'application/json',
622
+ 'x-api-key': apiKey,
623
+ 'anthropic-version': '2023-06-01',
624
+ },
625
+ body: JSON.stringify({
626
+ model: 'claude-haiku-4-5-20251001',
627
+ max_tokens: 500,
628
+ messages: [{
629
+ role: 'user',
630
+ content: `Summarize this Claude Code session in 2-3 sentences. What was the user trying to do? What's the current status? Answer in the same language as the session content.\n\n${contextEntries.join('\n')}`,
631
+ }],
632
+ }),
633
+ });
634
+ if (res.ok) {
635
+ const data = await res.json();
636
+ summary = data.content?.[0]?.text || '';
637
+ }
638
+ }
639
+ } catch {}
640
+
641
+ // Format output
642
+ const header = `📋 ${project.name} / ${session.sessionId.slice(0, 8)}\n${entries.length} entries${session.gitBranch ? ` • ${session.gitBranch}` : ''}`;
643
+
644
+ const summaryBlock = summary
645
+ ? `\n\n📝 Summary:\n${summary}`
646
+ : '';
647
+
648
+ const rawBlock = `\n\n--- Recent ---\n${recentRaw.join('\n\n')}`;
649
+
650
+ const fullText = header + summaryBlock + rawBlock;
651
+ const chunks = splitMessage(fullText, 4000);
652
+ for (const chunk of chunks) {
653
+ await send(chatId, chunk);
654
+ }
655
+ }
656
+
440
657
  /**
441
658
  * Parse task creation input. Supports:
442
659
  * project-name instructions
@@ -444,6 +661,25 @@ async function sendSessionContent(chatId: number, projectName: string, sessionId
444
661
  * project-name -in 30m instructions
445
662
  * project-name -at 2024-01-01T10:00 instructions
446
663
  */
664
+ async function startTaskCreation(chatId: number) {
665
+ const projects = scanProjects();
666
+ if (projects.length === 0) {
667
+ await send(chatId, 'No projects configured. Add project roots in Settings.');
668
+ return;
669
+ }
670
+
671
+ const numbered = new Map<number, string>();
672
+ const lines = projects.slice(0, 15).map((p, i) => {
673
+ numbered.set(i + 1, p.name);
674
+ return `${i + 1}. ${p.name}`;
675
+ });
676
+
677
+ chatNumberedProjects.set(chatId, numbered);
678
+ chatListMode.set(chatId, 'task-create');
679
+
680
+ await send(chatId, `📝 New Task\n\nSelect project:\n${lines.join('\n')}`);
681
+ }
682
+
447
683
  async function handleNewTask(chatId: number, input: string) {
448
684
  if (!input) {
449
685
  await send(chatId,
@@ -754,6 +990,167 @@ async function handleTunnelPassword(chatId: number, password?: string, userMsgId
754
990
  }
755
991
  }
756
992
 
993
+ // ─── Docs ────────────────────────────────────────────────────
994
+
995
+ async function handleDocs(chatId: number, input: string) {
996
+ const settings = loadSettings();
997
+ if (String(chatId) !== settings.telegramChatId) { await send(chatId, '⛔ Unauthorized'); return; }
998
+
999
+ const docRoots = (settings.docRoots || []).map((r: string) => r.replace(/^~/, require('os').homedir()));
1000
+ if (docRoots.length === 0) {
1001
+ await send(chatId, '⚠️ No document directories configured.\nAdd them in Settings → Document Roots');
1002
+ return;
1003
+ }
1004
+
1005
+ const docRoot = docRoots[0];
1006
+ const { homedir: getHome } = require('os');
1007
+ const { join, extname } = require('path');
1008
+ const { existsSync, readFileSync, readdirSync } = require('fs');
1009
+
1010
+ // /docs <filename> — search and show file content
1011
+ if (input.trim()) {
1012
+ const query = input.trim().toLowerCase();
1013
+
1014
+ // Recursive search for matching .md files
1015
+ const matches: string[] = [];
1016
+ function searchDir(dir: string, depth: number) {
1017
+ if (depth > 5 || matches.length >= 5) return;
1018
+ try {
1019
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1020
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
1021
+ const full = join(dir, entry.name);
1022
+ if (entry.isDirectory()) {
1023
+ searchDir(full, depth + 1);
1024
+ } else if (entry.name.toLowerCase().includes(query) && extname(entry.name) === '.md') {
1025
+ matches.push(full);
1026
+ }
1027
+ }
1028
+ } catch {}
1029
+ }
1030
+ searchDir(docRoot, 0);
1031
+
1032
+ if (matches.length === 0) {
1033
+ await send(chatId, `No docs matching "${input.trim()}"`);
1034
+ return;
1035
+ }
1036
+
1037
+ // Show first match
1038
+ const filePath = matches[0];
1039
+ const relPath = filePath.replace(docRoot + '/', '');
1040
+ try {
1041
+ const content = readFileSync(filePath, 'utf-8');
1042
+ const preview = content.slice(0, 3500);
1043
+ const truncated = content.length > 3500 ? '\n\n... (truncated)' : '';
1044
+ await send(chatId, `📄 ${relPath}\n\n${preview}${truncated}`);
1045
+ if (matches.length > 1) {
1046
+ const others = matches.slice(1).map(m => ` ${m.replace(docRoot + '/', '')}`).join('\n');
1047
+ await send(chatId, `Other matches:\n${others}`);
1048
+ }
1049
+ } catch {
1050
+ await send(chatId, `Failed to read: ${relPath}`);
1051
+ }
1052
+ return;
1053
+ }
1054
+
1055
+ // /docs — show summary of latest Claude session for docs
1056
+ const hash = docRoot.replace(/\//g, '-');
1057
+ const claudeDir = join(getHome(), '.claude', 'projects', hash);
1058
+
1059
+ if (!existsSync(claudeDir)) {
1060
+ await send(chatId, `📖 Docs: ${docRoot.split('/').pop()}\n\nNo Claude sessions yet. Open Docs tab to start.`);
1061
+ return;
1062
+ }
1063
+
1064
+ // Find latest session
1065
+ let latestFile = '';
1066
+ let latestTime = 0;
1067
+ try {
1068
+ for (const f of readdirSync(claudeDir)) {
1069
+ if (!f.endsWith('.jsonl')) continue;
1070
+ const { statSync } = require('fs');
1071
+ const stat = statSync(join(claudeDir, f));
1072
+ if (stat.mtimeMs > latestTime) {
1073
+ latestTime = stat.mtimeMs;
1074
+ latestFile = f;
1075
+ }
1076
+ }
1077
+ } catch {}
1078
+
1079
+ if (!latestFile) {
1080
+ await send(chatId, `📖 Docs: ${docRoot.split('/').pop()}\n\nNo sessions found.`);
1081
+ return;
1082
+ }
1083
+
1084
+ const sessionId = latestFile.replace('.jsonl', '');
1085
+ const filePath = join(claudeDir, latestFile);
1086
+
1087
+ // Read recent entries
1088
+ let entries: string[] = [];
1089
+ try {
1090
+ const content = readFileSync(filePath, 'utf-8');
1091
+ const lines = content.split('\n').filter(Boolean);
1092
+ const recentLines = lines.slice(-30);
1093
+
1094
+ for (const line of recentLines) {
1095
+ try {
1096
+ const entry = JSON.parse(line);
1097
+ if (entry.type === 'human' || entry.role === 'user') {
1098
+ const text = typeof entry.message === 'string' ? entry.message : entry.message?.content?.[0]?.text || '';
1099
+ if (text) entries.push(`👤 ${text.slice(0, 200)}`);
1100
+ } else if (entry.type === 'assistant' && entry.message?.content) {
1101
+ for (const block of entry.message.content) {
1102
+ if (block.type === 'text' && block.text) {
1103
+ entries.push(`🤖 ${block.text.slice(0, 200)}`);
1104
+ } else if (block.type === 'tool_use') {
1105
+ entries.push(`🔧 ${block.name || 'tool'}`);
1106
+ }
1107
+ }
1108
+ }
1109
+ } catch {}
1110
+ }
1111
+ } catch {}
1112
+
1113
+ const recent = entries.slice(-8).join('\n\n');
1114
+ const header = `📖 Docs: ${docRoot.split('/').pop()}\n📋 Session: ${sessionId.slice(0, 12)}\n`;
1115
+
1116
+ // Try AI summary if available
1117
+ let summary = '';
1118
+ try {
1119
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1120
+ if (apiKey && entries.length > 2) {
1121
+ const contextText = entries.slice(-15).join('\n');
1122
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
1123
+ method: 'POST',
1124
+ headers: {
1125
+ 'Content-Type': 'application/json',
1126
+ 'x-api-key': apiKey,
1127
+ 'anthropic-version': '2023-06-01',
1128
+ },
1129
+ body: JSON.stringify({
1130
+ model: 'claude-haiku-4-5-20251001',
1131
+ max_tokens: 300,
1132
+ messages: [{
1133
+ role: 'user',
1134
+ content: `Summarize this Claude Code session in 2-3 sentences. What was the user working on? What's the current status? Answer in the same language as the content.\n\n${contextText}`,
1135
+ }],
1136
+ }),
1137
+ });
1138
+ if (res.ok) {
1139
+ const data = await res.json();
1140
+ summary = data.content?.[0]?.text || '';
1141
+ }
1142
+ }
1143
+ } catch {}
1144
+
1145
+ const summaryBlock = summary ? `\n📝 ${summary}\n` : '';
1146
+ const fullText = header + summaryBlock + '\n--- Recent ---\n' + recent;
1147
+
1148
+ const chunks = splitMessage(fullText, 4000);
1149
+ for (const chunk of chunks) {
1150
+ await send(chatId, chunk);
1151
+ }
1152
+ }
1153
+
757
1154
  // ─── Real-time Streaming ─────────────────────────────────────
758
1155
 
759
1156
  function bufferLogEntry(taskId: string, chatId: number, entry: TaskLogEntry) {
@@ -877,13 +1274,15 @@ async function setBotCommands(token: string) {
877
1274
  body: JSON.stringify({
878
1275
  commands: [
879
1276
  { command: 'tasks', description: 'List tasks' },
880
- { command: 'task', description: 'Create task: /task project prompt' },
1277
+ { command: 'task', description: 'Create task (interactive or /task project prompt)' },
881
1278
  { command: 'sessions', description: 'Browse sessions' },
882
1279
  { command: 'projects', description: 'List projects' },
883
1280
  { command: 'tunnel', description: 'Tunnel status' },
884
1281
  { command: 'tunnel_start', description: 'Start tunnel' },
885
1282
  { command: 'tunnel_stop', description: 'Stop tunnel' },
886
1283
  { command: 'tunnel_password', description: 'Get login password' },
1284
+ { command: 'peek', description: 'Session summary (AI + recent)' },
1285
+ { command: 'docs', description: 'Docs session summary / view file' },
887
1286
  { command: 'watch', description: 'Monitor session' },
888
1287
  { command: 'watchers', description: 'List watchers' },
889
1288
  { command: 'help', description: 'Show help' },
@@ -28,7 +28,7 @@ import { WebSocketServer, WebSocket } from 'ws';
28
28
  import * as pty from 'node-pty';
29
29
  import { execSync } from 'node:child_process';
30
30
  import { homedir } from 'node:os';
31
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
31
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
32
32
  import { join } from 'node:path';
33
33
 
34
34
  const PORT = Number(process.env.TERMINAL_PORT) || 3001;
@@ -116,6 +116,19 @@ function listTmuxSessions(): { name: string; created: string; attached: boolean;
116
116
 
117
117
  const MAX_SESSIONS = 10;
118
118
 
119
+ function getDefaultCwd(): string {
120
+ try {
121
+ const settingsPath = join(homedir(), '.forge', 'settings.yaml');
122
+ const raw = readFileSync(settingsPath, 'utf-8');
123
+ const match = raw.match(/projectRoots:\s*\n((?:\s+-\s+.+\n?)*)/);
124
+ if (match) {
125
+ const first = match[1].split('\n').map(l => l.replace(/^\s+-\s+/, '').trim()).filter(Boolean)[0];
126
+ if (first) return first.replace(/^~/, homedir());
127
+ }
128
+ } catch {}
129
+ return homedir();
130
+ }
131
+
119
132
  function createTmuxSession(cols: number, rows: number): string {
120
133
  // Auto-cleanup: if too many sessions, kill the oldest idle ones
121
134
  const existing = listTmuxSessions();
@@ -132,7 +145,7 @@ function createTmuxSession(cols: number, rows: number): string {
132
145
  const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
133
146
  const name = `${SESSION_PREFIX}${id}`;
134
147
  execSync(`${TMUX} new-session -d -s ${name} -x ${cols} -y ${rows}`, {
135
- cwd: homedir(),
148
+ cwd: getDefaultCwd(),
136
149
  env: { ...process.env, TERM: 'xterm-256color' },
137
150
  });
138
151
  // Enable mouse scrolling and set large scrollback buffer
@@ -305,7 +318,26 @@ wss.on('connection', (ws: WebSocket) => {
305
318
  const cols = parsed.cols || 120;
306
319
  const rows = parsed.rows || 30;
307
320
  try {
308
- const name = createTmuxSession(cols, rows);
321
+ // Support fixed session name (e.g. mw-docs-claude)
322
+ let name: string;
323
+ if (parsed.sessionName && parsed.sessionName.startsWith(SESSION_PREFIX)) {
324
+ // Create with fixed name if it doesn't exist, otherwise attach
325
+ if (tmuxSessionExists(parsed.sessionName)) {
326
+ attachToTmux(parsed.sessionName, cols, rows);
327
+ break;
328
+ }
329
+ name = parsed.sessionName;
330
+ execSync(`${TMUX} new-session -d -s ${name} -x ${cols} -y ${rows}`, {
331
+ cwd: homedir(),
332
+ env: { ...process.env, TERM: 'xterm-256color' },
333
+ });
334
+ try {
335
+ execSync(`${TMUX} set-option -t ${name} mouse on 2>/dev/null`);
336
+ execSync(`${TMUX} set-option -t ${name} history-limit 50000 2>/dev/null`);
337
+ } catch {}
338
+ } else {
339
+ name = createTmuxSession(cols, rows);
340
+ }
309
341
  createdAt.set(ws, { session: name, time: Date.now() });
310
342
  attachToTmux(name, cols, rows);
311
343
  } catch (e: unknown) {
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/types/routes.d.ts";
3
+ import "./.next/dev/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.10",
3
+ "version": "0.2.0",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -42,6 +42,7 @@
42
42
  "react": "^19.2.4",
43
43
  "react-dom": "^19.2.4",
44
44
  "react-markdown": "^10.1.0",
45
+ "remark-gfm": "^4.0.1",
45
46
  "ws": "^8.19.0",
46
47
  "yaml": "^2.8.2"
47
48
  },