@aion0/forge 0.2.1 → 0.2.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.
@@ -39,6 +39,9 @@ const chatListMode = new Map<number, 'tasks' | 'projects' | 'sessions' | 'task-c
39
39
  // Pending task creation: waiting for prompt text
40
40
  const pendingTaskProject = new Map<number, { name: string; path: string }>(); // chatId → project
41
41
 
42
+ // Pending note: waiting for content
43
+ const pendingNote = new Set<number>(); // chatIds waiting for note content
44
+
42
45
  // Buffer for streaming logs
43
46
  const logBuffers = new Map<string, { entries: string[]; timer: ReturnType<typeof setTimeout> | null }>();
44
47
 
@@ -112,10 +115,25 @@ async function poll() {
112
115
 
113
116
  async function handleMessage(msg: any) {
114
117
  const chatId = msg.chat.id;
118
+
119
+ // Whitelist check — only allow configured chat IDs, block all if not configured
120
+ const settings = loadSettings();
121
+ const allowedIds = settings.telegramChatId.split(',').map((s: string) => s.trim()).filter(Boolean);
122
+ if (allowedIds.length === 0 || !allowedIds.includes(String(chatId))) {
123
+ return;
124
+ }
125
+
115
126
  // Message received (logged silently)
116
127
  const text: string = msg.text.trim();
117
128
  const replyTo = msg.reply_to_message?.message_id;
118
129
 
130
+ // Check if waiting for note content
131
+ if (pendingNote.has(chatId) && !text.startsWith('/')) {
132
+ pendingNote.delete(chatId);
133
+ await sendNoteToDocsClaude(chatId, text);
134
+ return;
135
+ }
136
+
119
137
  // Check if waiting for task prompt
120
138
  const pending = pendingTaskProject.get(chatId);
121
139
  if (pending && !text.startsWith('/')) {
@@ -186,6 +204,7 @@ async function handleMessage(msg: any) {
186
204
  if (text.startsWith('/')) {
187
205
  // Any new command cancels pending states
188
206
  pendingTaskProject.delete(chatId);
207
+ pendingNote.delete(chatId);
189
208
 
190
209
  const [cmd, ...args] = text.split(/\s+/);
191
210
  switch (cmd) {
@@ -238,6 +257,10 @@ async function handleMessage(msg: any) {
238
257
  case '/doc':
239
258
  await handleDocs(chatId, args.join(' '));
240
259
  break;
260
+ case '/note':
261
+ case '/docs_write':
262
+ await handleDocsWrite(chatId, args.join(' '));
263
+ break;
241
264
  case '/cancel':
242
265
  await handleCancel(chatId, args[0]);
243
266
  break;
@@ -293,7 +316,8 @@ async function sendHelp(chatId: number) {
293
316
  `📝 Submit task:\nproject-name: your instructions\n\n` +
294
317
  `👀 /peek [project] [sessionId] — session summary\n` +
295
318
  `📖 /docs — docs session summary\n` +
296
- `/docs <filename> — view doc file\n\n` +
319
+ `/docs <filename> — view doc file\n` +
320
+ `📝 /note — quick note to docs claude\n\n` +
297
321
  `🔧 /cancel <id> /retry <id>\n` +
298
322
  `/projects — list projects\n\n` +
299
323
  `🌐 /tunnel — tunnel status\n` +
@@ -1151,6 +1175,79 @@ async function handleDocs(chatId: number, input: string) {
1151
1175
  }
1152
1176
  }
1153
1177
 
1178
+ // ─── Docs Write (Quick Notes) ────────────────────────────────
1179
+
1180
+ async function handleDocsWrite(chatId: number, content: string) {
1181
+ const settings = loadSettings();
1182
+ if (String(chatId) !== settings.telegramChatId) { await send(chatId, '⛔ Unauthorized'); return; }
1183
+
1184
+ if (!content) {
1185
+ pendingNote.add(chatId);
1186
+ await send(chatId, '📝 Send your note content:');
1187
+ return;
1188
+ }
1189
+
1190
+ await sendNoteToDocsClaude(chatId, content);
1191
+ }
1192
+
1193
+ async function sendNoteToDocsClaude(chatId: number, content: string) {
1194
+ const settings = loadSettings();
1195
+ const docRoots = (settings.docRoots || []).map((r: string) => r.replace(/^~/, require('os').homedir()));
1196
+
1197
+ if (docRoots.length === 0) {
1198
+ await send(chatId, '⚠️ No document directories configured.');
1199
+ return;
1200
+ }
1201
+
1202
+ const { execSync, spawnSync } = require('child_process');
1203
+ const { writeFileSync, unlinkSync } = require('fs');
1204
+ const { join } = require('path');
1205
+ const { homedir } = require('os');
1206
+ const SESSION_NAME = 'mw-docs-claude';
1207
+
1208
+ // Check if the docs tmux session exists
1209
+ let sessionExists = false;
1210
+ try {
1211
+ execSync(`tmux has-session -t ${SESSION_NAME} 2>/dev/null`);
1212
+ sessionExists = true;
1213
+ } catch {}
1214
+
1215
+ if (!sessionExists) {
1216
+ await send(chatId, '⚠️ Docs Claude session not running. Open the Docs tab first to start it.');
1217
+ return;
1218
+ }
1219
+
1220
+ // Check if Claude is the active process (not shell)
1221
+ let paneCmd = '';
1222
+ try {
1223
+ paneCmd = execSync(`tmux display-message -p -t ${SESSION_NAME} '#{pane_current_command}'`, { encoding: 'utf-8', timeout: 2000 }).trim();
1224
+ } catch {}
1225
+
1226
+ if (paneCmd === 'zsh' || paneCmd === 'bash' || paneCmd === 'fish' || !paneCmd) {
1227
+ await send(chatId, '⚠️ Claude is not running in the Docs session. Open the Docs tab and start Claude first.');
1228
+ return;
1229
+ }
1230
+
1231
+ // Write content to a temp file, then use tmux to send a prompt referencing it
1232
+ const tmpFile = join(homedir(), '.forge', '.note-tmp.txt');
1233
+ try {
1234
+ writeFileSync(tmpFile, content, 'utf-8');
1235
+
1236
+ // Send a single-line prompt to Claude via tmux send-keys using the temp file
1237
+ const prompt = `Please read the file ${tmpFile} and save its content as a note in the appropriate location in my docs. Analyze the content to determine the best file and location. After saving, delete the temp file.`;
1238
+
1239
+ // Use tmux send-keys with literal flag to avoid interpretation issues
1240
+ spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, '-l', prompt], { timeout: 5000 });
1241
+ // Send Enter separately
1242
+ spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, 'Enter'], { timeout: 2000 });
1243
+
1244
+ await send(chatId, `📝 Note sent to Docs Claude:\n\n${content.slice(0, 200)}${content.length > 200 ? '...' : ''}`);
1245
+ } catch (err) {
1246
+ try { unlinkSync(tmpFile); } catch {}
1247
+ await send(chatId, '❌ Failed to send note to Claude session');
1248
+ }
1249
+ }
1250
+
1154
1251
  // ─── Real-time Streaming ─────────────────────────────────────
1155
1252
 
1156
1253
  function bufferLogEntry(taskId: string, chatId: number, entry: TaskLogEntry) {
@@ -1283,6 +1380,7 @@ async function setBotCommands(token: string) {
1283
1380
  { command: 'tunnel_password', description: 'Get login password' },
1284
1381
  { command: 'peek', description: 'Session summary (AI + recent)' },
1285
1382
  { command: 'docs', description: 'Docs session summary / view file' },
1383
+ { command: 'note', description: 'Quick note to docs Claude' },
1286
1384
  { command: 'watch', description: 'Monitor session' },
1287
1385
  { command: 'watchers', description: 'List watchers' },
1288
1386
  { command: 'help', description: 'Show help' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.2.1",
3
+ "version": "0.2.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": {
@@ -34,6 +34,7 @@
34
34
  "@auth/core": "^0.34.3",
35
35
  "@xterm/addon-fit": "^0.11.0",
36
36
  "@xterm/xterm": "^6.0.0",
37
+ "@xyflow/react": "^12.10.1",
37
38
  "ai": "^6.0.116",
38
39
  "better-sqlite3": "^12.6.2",
39
40
  "next": "^16.1.6",