@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.
- package/dist/index.js +95 -100
- package/package.json +1 -1
- 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
|
|
12
|
-
var
|
|
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
|
|
197
|
+
throw new Error("No API key found. Set ZAI_API_KEY env var.");
|
|
182
198
|
}
|
|
183
|
-
async function callGLM(messages
|
|
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
|
|
236
|
+
return callGLM(messages);
|
|
221
237
|
}
|
|
222
238
|
return choice.message?.content || "(no response)";
|
|
223
239
|
}
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
282
|
-
const
|
|
283
|
-
return
|
|
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(`
|
|
267
|
+
console.log(`Chat dir: ${CHAT_DIR}`);
|
|
288
268
|
console.log(`Memory: ${MEMORY_FILE}`);
|
|
289
|
-
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
308
|
+
setStatus("error");
|
|
309
|
+
writeOutput(`Error: ${error.message}`);
|
|
324
310
|
}
|
|
325
311
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
323
|
+
watcher.close();
|
|
324
|
+
process.exit(0);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
332
327
|
main().catch(console.error);
|
package/package.json
CHANGED
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
|
|
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
|
-
* -
|
|
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
|
|
19
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
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
|
|
27
|
-
const
|
|
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
|
|
247
|
+
throw new Error('No API key found. Set ZAI_API_KEY env var.');
|
|
225
248
|
}
|
|
226
249
|
|
|
227
|
-
async function callGLM(messages: Message[]
|
|
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
|
-
|
|
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
|
-
//
|
|
301
|
+
// Process Message
|
|
282
302
|
// ====================================================================
|
|
283
303
|
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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(`
|
|
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
|
-
//
|
|
372
|
-
|
|
347
|
+
// Ensure directories exist
|
|
348
|
+
ensureDir();
|
|
373
349
|
|
|
374
|
-
// Initialize memory
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
while (true) {
|
|
374
|
+
async function processIncoming() {
|
|
387
375
|
try {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
if
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
424
|
-
|
|
396
|
+
setStatus('idle');
|
|
397
|
+
console.log(`[${new Date().toISOString()}] Response sent`);
|
|
425
398
|
} catch (error) {
|
|
426
399
|
console.error('Error:', error);
|
|
427
|
-
|
|
400
|
+
setStatus('error');
|
|
401
|
+
writeOutput(`Error: ${(error as Error).message}`);
|
|
428
402
|
}
|
|
429
403
|
}
|
|
430
|
-
}
|
|
431
404
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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);
|