@ebowwa/channel-ssh 1.0.1 → 1.0.2

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.
Files changed (3) hide show
  1. package/dist/index.js +95 -100
  2. package/package.json +1 -1
  3. package/src/index.ts +132 -147
package/dist/index.js CHANGED
@@ -3,13 +3,29 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { execSync } from "child_process";
6
- import { existsSync, readFileSync, writeFileSync } from "fs";
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, watch } from "fs";
7
7
  import { homedir } from "os";
8
8
  import { join } from "path";
9
- var SESSION_NAME = process.env.SSH_CHAT_SESSION || "ssh-chat";
10
9
  var GLM_API_ENDPOINT = "https://api.z.ai/api/coding/paas/v4/chat/completions";
11
- var MEMORY_FILE = process.env.SSH_MEMORY_FILE || join(homedir(), ".ssh-chat-memory.json");
12
- var PROMPTS_FILE = process.env.PROMPTS_FILE || join(homedir(), ".ssh-chat-prompts.json");
10
+ var CHAT_DIR = process.env.SSH_CHAT_DIR || join(homedir(), ".ssh-chat");
11
+ var IN_FILE = join(CHAT_DIR, "in");
12
+ var OUT_FILE = join(CHAT_DIR, "out");
13
+ var STATUS_FILE = join(CHAT_DIR, "status");
14
+ var MEMORY_FILE = join(CHAT_DIR, "memory.json");
15
+ function ensureDir() {
16
+ if (!existsSync(CHAT_DIR)) {
17
+ mkdirSync(CHAT_DIR, { recursive: true });
18
+ }
19
+ }
20
+ function setStatus(status) {
21
+ writeFileSync(STATUS_FILE, JSON.stringify({ status, timestamp: Date.now() }));
22
+ }
23
+ function writeOutput(text) {
24
+ const timestamp = new Date().toISOString();
25
+ writeFileSync(OUT_FILE, `[${timestamp}]
26
+ ${text}
27
+ `);
28
+ }
13
29
 
14
30
  class ConversationMemory {
15
31
  file;
@@ -178,9 +194,9 @@ function getAPIKey() {
178
194
  }
179
195
  } catch {}
180
196
  }
181
- throw new Error("No API key found. Set ZAI_API_KEY or ZAI_API_KEYS env var.");
197
+ throw new Error("No API key found. Set ZAI_API_KEY env var.");
182
198
  }
183
- async function callGLM(messages, tools) {
199
+ async function callGLM(messages) {
184
200
  const apiKey = getAPIKey();
185
201
  const response = await fetch(GLM_API_ENDPOINT, {
186
202
  method: "POST",
@@ -217,116 +233,95 @@ async function callGLM(messages, tools) {
217
233
  messages.push({ role: "user", content: `Tool results:
218
234
  ${toolResults.join(`
219
235
  `)}`, timestamp: Date.now() });
220
- return callGLM(messages, tools);
236
+ return callGLM(messages);
221
237
  }
222
238
  return choice.message?.content || "(no response)";
223
239
  }
224
- function tmux(args) {
225
- try {
226
- return execSync(`tmux ${args}`, { encoding: "utf-8" }).trim();
227
- } catch (e) {
228
- return e.stdout?.toString().trim() || "";
229
- }
230
- }
231
- function sessionExists() {
232
- const result = tmux(`has-session -t ${SESSION_NAME} 2>/dev/null`);
233
- return !result.includes("no session");
234
- }
235
- function createSession() {
236
- if (!sessionExists()) {
237
- tmux(`new-session -d -s ${SESSION_NAME} -x 200 -y 50`);
238
- tmux(`send-keys -t ${SESSION_NAME} '\uD83E\uDD16 SSH Chat Channel - Type your message and press Enter' Enter`);
239
- tmux(`send-keys -t ${SESSION_NAME} '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501' Enter`);
240
- tmux(`send-keys -t ${SESSION_NAME} '' Enter`);
241
- console.log(`Created tmux session: ${SESSION_NAME}`);
242
- }
243
- }
244
- function getPaneContent() {
245
- return tmux(`capture-pane -t ${SESSION_NAME} -p -S -100`);
246
- }
247
- function sendToPane(text) {
248
- const lines = text.split(`
249
- `);
250
- tmux(`send-keys -t ${SESSION_NAME} '' Enter`);
251
- for (const line of lines) {
252
- const escaped = line.replace(/["'$`\\]/g, "\\$&");
253
- tmux(`send-keys -t ${SESSION_NAME} '${escaped}' Enter`);
254
- }
255
- tmux(`send-keys -t ${SESSION_NAME} '' Enter`);
256
- tmux(`send-keys -t ${SESSION_NAME} '\uD83D\uDC64 You: '`);
257
- }
258
- var lastContent = "";
259
- function detectNewInput() {
260
- const currentContent = getPaneContent();
261
- if (currentContent === lastContent) {
262
- return null;
263
- }
264
- const lastLines = lastContent.split(`
265
- `);
266
- const currentLines = currentContent.split(`
267
- `);
268
- const newLines = [];
269
- let foundLast = false;
270
- for (const line of currentLines) {
271
- if (!foundLast) {
272
- if (line === lastLines[lastLines.length - 1]) {
273
- foundLast = true;
274
- }
275
- } else {
276
- if (!line.includes("\uD83D\uDC64 You:") && line.trim()) {
277
- newLines.push(line.trim());
278
- }
240
+ async function processMessage(input, memory) {
241
+ if (input.startsWith("/")) {
242
+ if (input === "/clear") {
243
+ memory.clear();
244
+ return "\uD83D\uDDD1\uFE0F Memory cleared.";
245
+ }
246
+ if (input === "/help") {
247
+ return `Commands:
248
+ /clear - Clear conversation memory
249
+ /help - Show this help
250
+ /status - Show system status
251
+
252
+ Just type a message to chat with AI.`;
253
+ }
254
+ if (input === "/status") {
255
+ return `Status: running
256
+ Memory file: ${MEMORY_FILE}
257
+ Chat dir: ${CHAT_DIR}`;
279
258
  }
259
+ return `Unknown command: ${input}. Type /help for available commands.`;
280
260
  }
281
- lastContent = currentContent;
282
- const input = newLines.join(" ").trim();
283
- return input || null;
261
+ memory.add("user", input);
262
+ const messages = memory.getContext(20);
263
+ return await callGLM(messages);
284
264
  }
285
265
  async function main() {
286
266
  console.log("\uD83E\uDD16 SSH Chat Channel starting...");
287
- console.log(`Session: ${SESSION_NAME}`);
267
+ console.log(`Chat dir: ${CHAT_DIR}`);
288
268
  console.log(`Memory: ${MEMORY_FILE}`);
289
- createSession();
269
+ console.log("");
270
+ console.log("Usage:");
271
+ console.log(` Write message: echo "your message" > ${IN_FILE}`);
272
+ console.log(` Read response: cat ${OUT_FILE}`);
273
+ console.log("");
274
+ ensureDir();
290
275
  const memory = new ConversationMemory(MEMORY_FILE);
291
- memory.add("system", `You are an AI assistant accessible via SSH tmux session.
276
+ memory.add("system", `You are an AI assistant accessible via SSH.
292
277
  You are helpful, concise, and can execute tools to help the user.
293
278
  This is a private SSH channel separate from any Telegram or other chat interfaces.`);
294
- console.log("Ready. Monitoring tmux session for input...");
295
- console.log(`Attach with: tmux attach -t ${SESSION_NAME}`);
296
- while (true) {
279
+ if (!existsSync(IN_FILE))
280
+ writeFileSync(IN_FILE, "");
281
+ if (!existsSync(OUT_FILE))
282
+ writeFileSync(OUT_FILE, `Ready. Send a message.
283
+ `);
284
+ setStatus("idle");
285
+ let lastContent = "";
286
+ console.log("Watching for messages...");
287
+ const watcher = watch(CHAT_DIR, (eventType, filename) => {
288
+ if (filename === "in" && eventType === "change") {
289
+ processIncoming();
290
+ }
291
+ });
292
+ async function processIncoming() {
297
293
  try {
298
- const input = detectNewInput();
299
- if (input && input.length > 0) {
300
- if (input.startsWith("/")) {
301
- if (input === "/clear") {
302
- memory.clear();
303
- sendToPane("\uD83D\uDDD1\uFE0F Memory cleared.");
304
- } else if (input === "/exit" || input === "/quit") {
305
- sendToPane("\uD83D\uDC4B Goodbye!");
306
- break;
307
- } else {
308
- sendToPane(`Unknown command: ${input}`);
309
- }
310
- continue;
311
- }
312
- console.log(`[${new Date().toISOString()}] Input: ${input.slice(0, 50)}...`);
313
- memory.add("user", input);
314
- const messages = memory.getContext(20);
315
- const response = await callGLM(messages, TOOLS);
316
- memory.add("assistant", response);
317
- sendToPane(`\uD83E\uDD16 AI: ${response}`);
318
- console.log(`[${new Date().toISOString()}] Response sent`);
319
- }
320
- await new Promise((r) => setTimeout(r, 500));
294
+ const content = readFileSync(IN_FILE, "utf-8").trim();
295
+ if (!content || content === lastContent)
296
+ return;
297
+ lastContent = content;
298
+ setStatus("processing");
299
+ console.log(`[${new Date().toISOString()}] Processing: ${content.slice(0, 50)}...`);
300
+ writeFileSync(IN_FILE, "");
301
+ const response = await processMessage(content, memory);
302
+ writeOutput(response);
303
+ memory.add("assistant", response);
304
+ setStatus("idle");
305
+ console.log(`[${new Date().toISOString()}] Response sent`);
321
306
  } catch (error) {
322
307
  console.error("Error:", error);
323
- await new Promise((r) => setTimeout(r, 2000));
308
+ setStatus("error");
309
+ writeOutput(`Error: ${error.message}`);
324
310
  }
325
311
  }
326
- }
327
- process.on("SIGINT", () => {
328
- console.log(`
312
+ setInterval(() => {
313
+ try {
314
+ const content = readFileSync(IN_FILE, "utf-8").trim();
315
+ if (content && content !== lastContent) {
316
+ processIncoming();
317
+ }
318
+ } catch {}
319
+ }, 500);
320
+ process.on("SIGINT", () => {
321
+ console.log(`
329
322
  Shutting down...`);
330
- process.exit(0);
331
- });
323
+ watcher.close();
324
+ process.exit(0);
325
+ });
326
+ }
332
327
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebowwa/channel-ssh",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "SSH tmux channel for GLM Daemon - separate from Telegram",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -2,29 +2,54 @@
2
2
  /**
3
3
  * SSH Channel for GLM Daemon
4
4
  *
5
- * Provides AI chat via SSH tmux session - completely separate from Telegram
5
+ * Provides AI chat via file-based IPC - works with systemd and tmux
6
6
  *
7
7
  * Usage:
8
- * bun run src/index.ts
8
+ * Direct: bun run src/index.ts
9
+ * With tmux wrapper: tmux new-session -s ssh-chat "channel-ssh-interactive"
10
+ *
11
+ * Communication:
12
+ * IN_FILE: ~/.ssh-chat/in (user writes messages here)
13
+ * OUT_FILE: ~/.ssh-chat/out (AI responses here)
14
+ * STATUS_FILE: ~/.ssh-chat/status (processing/idle)
9
15
  *
10
16
  * Features:
11
- * - Creates/attaches to tmux session "ssh-chat"
12
- * - Monitors pane for user input (lines ending with Enter)
17
+ * - File-based IPC for systemd compatibility
13
18
  * - GLM-4.7 AI responses
14
19
  * - Separate conversation memory from Telegram
15
20
  * - Tool support (read_file, run_command, etc.)
16
21
  */
17
22
 
18
- import { execSync, spawn } from 'child_process';
19
- import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
20
- import { getStore } from '@ebowwa/structured-prompts';
23
+ import { execSync } from 'child_process';
24
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, watch } from 'fs';
21
25
  import { homedir } from 'os';
22
- import { join } from 'path';
26
+ import { join, dirname } from 'path';
23
27
 
24
- const SESSION_NAME = process.env.SSH_CHAT_SESSION || 'ssh-chat';
25
28
  const GLM_API_ENDPOINT = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
26
- const MEMORY_FILE = process.env.SSH_MEMORY_FILE || join(homedir(), '.ssh-chat-memory.json');
27
- const PROMPTS_FILE = process.env.PROMPTS_FILE || join(homedir(), '.ssh-chat-prompts.json');
29
+ const CHAT_DIR = process.env.SSH_CHAT_DIR || join(homedir(), '.ssh-chat');
30
+ const IN_FILE = join(CHAT_DIR, 'in');
31
+ const OUT_FILE = join(CHAT_DIR, 'out');
32
+ const STATUS_FILE = join(CHAT_DIR, 'status');
33
+ const MEMORY_FILE = join(CHAT_DIR, 'memory.json');
34
+
35
+ // ====================================================================
36
+ // Setup
37
+ // ====================================================================
38
+
39
+ function ensureDir(): void {
40
+ if (!existsSync(CHAT_DIR)) {
41
+ mkdirSync(CHAT_DIR, { recursive: true });
42
+ }
43
+ }
44
+
45
+ function setStatus(status: 'idle' | 'processing' | 'error'): void {
46
+ writeFileSync(STATUS_FILE, JSON.stringify({ status, timestamp: Date.now() }));
47
+ }
48
+
49
+ function writeOutput(text: string): void {
50
+ const timestamp = new Date().toISOString();
51
+ writeFileSync(OUT_FILE, `[${timestamp}]\n${text}\n`);
52
+ }
28
53
 
29
54
  // ====================================================================
30
55
  // Conversation Memory (Separate from Telegram)
@@ -206,11 +231,9 @@ async function executeTool(name: string, args: Record<string, unknown>): Promise
206
231
  // ====================================================================
207
232
 
208
233
  function getAPIKey(): string {
209
- // Try environment variable first
210
234
  const envKey = process.env.ZAI_API_KEY || process.env.GLM_API_KEY;
211
235
  if (envKey) return envKey;
212
236
 
213
- // Try rolling keys
214
237
  const keysJson = process.env.ZAI_API_KEYS;
215
238
  if (keysJson) {
216
239
  try {
@@ -221,10 +244,10 @@ function getAPIKey(): string {
221
244
  } catch {}
222
245
  }
223
246
 
224
- throw new Error('No API key found. Set ZAI_API_KEY or ZAI_API_KEYS env var.');
247
+ throw new Error('No API key found. Set ZAI_API_KEY env var.');
225
248
  }
226
249
 
227
- async function callGLM(messages: Message[], tools: typeof TOOLS): Promise<string> {
250
+ async function callGLM(messages: Message[]): Promise<string> {
228
251
  const apiKey = getAPIKey();
229
252
 
230
253
  const response = await fetch(GLM_API_ENDPOINT, {
@@ -261,178 +284,140 @@ async function callGLM(messages: Message[], tools: typeof TOOLS): Promise<string
261
284
  for (const tc of choice.message.tool_calls) {
262
285
  const toolName = tc.function?.name;
263
286
  const toolArgs = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
264
-
265
287
  const result = await executeTool(toolName, toolArgs);
266
288
  toolResults.push(`[${toolName}]: ${result}`);
267
289
  }
268
290
 
269
- // Continue conversation with tool results
270
291
  messages.push({ role: 'assistant', content: '', timestamp: Date.now() });
271
292
  messages.push({ role: 'user', content: `Tool results:\n${toolResults.join('\n')}`, timestamp: Date.now() });
272
293
 
273
- // Recursive call for final response
274
- return callGLM(messages, tools);
294
+ return callGLM(messages);
275
295
  }
276
296
 
277
297
  return choice.message?.content || '(no response)';
278
298
  }
279
299
 
280
300
  // ====================================================================
281
- // Tmux Interface
301
+ // Process Message
282
302
  // ====================================================================
283
303
 
284
- function tmux(args: string): string {
285
- try {
286
- return execSync(`tmux ${args}`, { encoding: 'utf-8' }).trim();
287
- } catch (e: any) {
288
- return e.stdout?.toString().trim() || '';
289
- }
290
- }
291
-
292
- function sessionExists(): boolean {
293
- const result = tmux(`has-session -t ${SESSION_NAME} 2>/dev/null`);
294
- return !result.includes('no session');
295
- }
296
-
297
- function createSession(): void {
298
- if (!sessionExists()) {
299
- tmux(`new-session -d -s ${SESSION_NAME} -x 200 -y 50`);
300
- tmux(`send-keys -t ${SESSION_NAME} '🤖 SSH Chat Channel - Type your message and press Enter' Enter`);
301
- tmux(`send-keys -t ${SESSION_NAME} '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' Enter`);
302
- tmux(`send-keys -t ${SESSION_NAME} '' Enter`);
303
- console.log(`Created tmux session: ${SESSION_NAME}`);
304
- }
305
- }
306
-
307
- function getPaneContent(): string {
308
- return tmux(`capture-pane -t ${SESSION_NAME} -p -S -100`);
309
- }
310
-
311
- function sendToPane(text: string): void {
312
- // Format and send response
313
- const lines = text.split('\n');
314
- tmux(`send-keys -t ${SESSION_NAME} '' Enter`);
315
- for (const line of lines) {
316
- // Escape special characters for tmux
317
- const escaped = line.replace(/["'$`\\]/g, '\\$&');
318
- tmux(`send-keys -t ${SESSION_NAME} '${escaped}' Enter`);
319
- }
320
- tmux(`send-keys -t ${SESSION_NAME} '' Enter`);
321
- tmux(`send-keys -t ${SESSION_NAME} '👤 You: '`);
322
- }
323
-
324
- // Track last seen content to detect new input
325
- let lastContent = '';
326
-
327
- function detectNewInput(): string | null {
328
- const currentContent = getPaneContent();
329
-
330
- if (currentContent === lastContent) {
331
- return null;
332
- }
333
-
334
- // Find new lines
335
- const lastLines = lastContent.split('\n');
336
- const currentLines = currentContent.split('\n');
337
-
338
- // Get lines added after last check
339
- const newLines: string[] = [];
340
- let foundLast = false;
304
+ async function processMessage(input: string, memory: ConversationMemory): Promise<string> {
305
+ // Handle commands
306
+ if (input.startsWith('/')) {
307
+ if (input === '/clear') {
308
+ memory.clear();
309
+ return '🗑️ Memory cleared.';
310
+ }
311
+ if (input === '/help') {
312
+ return `Commands:
313
+ /clear - Clear conversation memory
314
+ /help - Show this help
315
+ /status - Show system status
341
316
 
342
- for (const line of currentLines) {
343
- if (!foundLast) {
344
- if (line === lastLines[lastLines.length - 1]) {
345
- foundLast = true;
346
- }
347
- } else {
348
- // Skip prompt line
349
- if (!line.includes('👤 You:') && line.trim()) {
350
- newLines.push(line.trim());
351
- }
317
+ Just type a message to chat with AI.`;
318
+ }
319
+ if (input === '/status') {
320
+ return `Status: running
321
+ Memory file: ${MEMORY_FILE}
322
+ Chat dir: ${CHAT_DIR}`;
352
323
  }
324
+ return `Unknown command: ${input}. Type /help for available commands.`;
353
325
  }
354
326
 
355
- lastContent = currentContent;
356
-
357
- // Combine new lines as input
358
- const input = newLines.join(' ').trim();
359
- return input || null;
327
+ // Regular message - get AI response
328
+ memory.add('user', input);
329
+ const messages = memory.getContext(20);
330
+ return await callGLM(messages);
360
331
  }
361
332
 
362
333
  // ====================================================================
363
- // Main Loop
334
+ // Main Loop - File Watcher
364
335
  // ====================================================================
365
336
 
366
337
  async function main() {
367
338
  console.log('🤖 SSH Chat Channel starting...');
368
- console.log(`Session: ${SESSION_NAME}`);
339
+ console.log(`Chat dir: ${CHAT_DIR}`);
369
340
  console.log(`Memory: ${MEMORY_FILE}`);
341
+ console.log('');
342
+ console.log('Usage:');
343
+ console.log(` Write message: echo "your message" > ${IN_FILE}`);
344
+ console.log(` Read response: cat ${OUT_FILE}`);
345
+ console.log('');
370
346
 
371
- // Create tmux session
372
- createSession();
347
+ // Ensure directories exist
348
+ ensureDir();
373
349
 
374
- // Initialize memory (separate from Telegram)
350
+ // Initialize memory
375
351
  const memory = new ConversationMemory(MEMORY_FILE);
376
-
377
- // Add system prompt
378
- memory.add('system', `You are an AI assistant accessible via SSH tmux session.
352
+ memory.add('system', `You are an AI assistant accessible via SSH.
379
353
  You are helpful, concise, and can execute tools to help the user.
380
354
  This is a private SSH channel separate from any Telegram or other chat interfaces.`);
381
355
 
382
- console.log('Ready. Monitoring tmux session for input...');
383
- console.log(`Attach with: tmux attach -t ${SESSION_NAME}`);
356
+ // Create empty files if they don't exist
357
+ if (!existsSync(IN_FILE)) writeFileSync(IN_FILE, '');
358
+ if (!existsSync(OUT_FILE)) writeFileSync(OUT_FILE, 'Ready. Send a message.\n');
359
+
360
+ setStatus('idle');
361
+
362
+ // Track last processed content
363
+ let lastContent = '';
364
+
365
+ console.log('Watching for messages...');
366
+
367
+ // Watch for file changes
368
+ const watcher = watch(CHAT_DIR, (eventType, filename) => {
369
+ if (filename === 'in' && eventType === 'change') {
370
+ processIncoming();
371
+ }
372
+ });
384
373
 
385
- // Main loop
386
- while (true) {
374
+ async function processIncoming() {
387
375
  try {
388
- const input = detectNewInput();
389
-
390
- if (input && input.length > 0) {
391
- // Skip commands
392
- if (input.startsWith('/')) {
393
- if (input === '/clear') {
394
- memory.clear();
395
- sendToPane('🗑️ Memory cleared.');
396
- } else if (input === '/exit' || input === '/quit') {
397
- sendToPane('👋 Goodbye!');
398
- break;
399
- } else {
400
- sendToPane(`Unknown command: ${input}`);
401
- }
402
- continue;
403
- }
404
-
405
- console.log(`[${new Date().toISOString()}] Input: ${input.slice(0, 50)}...`);
406
-
407
- // Add user message to memory
408
- memory.add('user', input);
409
-
410
- // Get AI response
411
- const messages = memory.getContext(20);
412
- const response = await callGLM(messages, TOOLS);
413
-
414
- // Add response to memory
415
- memory.add('assistant', response);
416
-
417
- // Send to tmux
418
- sendToPane(`🤖 AI: ${response}`);
419
-
420
- console.log(`[${new Date().toISOString()}] Response sent`);
421
- }
376
+ const content = readFileSync(IN_FILE, 'utf-8').trim();
377
+
378
+ // Skip if same as last or empty
379
+ if (!content || content === lastContent) return;
380
+
381
+ lastContent = content;
382
+ setStatus('processing');
383
+
384
+ console.log(`[${new Date().toISOString()}] Processing: ${content.slice(0, 50)}...`);
385
+
386
+ // Clear input file after reading
387
+ writeFileSync(IN_FILE, '');
388
+
389
+ // Process message
390
+ const response = await processMessage(content, memory);
391
+
392
+ // Write response
393
+ writeOutput(response);
394
+ memory.add('assistant', response);
422
395
 
423
- // Poll every 500ms
424
- await new Promise(r => setTimeout(r, 500));
396
+ setStatus('idle');
397
+ console.log(`[${new Date().toISOString()}] Response sent`);
425
398
  } catch (error) {
426
399
  console.error('Error:', error);
427
- await new Promise(r => setTimeout(r, 2000));
400
+ setStatus('error');
401
+ writeOutput(`Error: ${(error as Error).message}`);
428
402
  }
429
403
  }
430
- }
431
404
 
432
- // Handle shutdown
433
- process.on('SIGINT', () => {
434
- console.log('\nShutting down...');
435
- process.exit(0);
436
- });
405
+ // Also poll as backup (watch can be unreliable)
406
+ setInterval(() => {
407
+ try {
408
+ const content = readFileSync(IN_FILE, 'utf-8').trim();
409
+ if (content && content !== lastContent) {
410
+ processIncoming();
411
+ }
412
+ } catch {}
413
+ }, 500);
414
+
415
+ // Keep running
416
+ process.on('SIGINT', () => {
417
+ console.log('\nShutting down...');
418
+ watcher.close();
419
+ process.exit(0);
420
+ });
421
+ }
437
422
 
438
423
  main().catch(console.error);