@ebowwa/channel-ssh 1.0.2 → 1.1.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.
Files changed (3) hide show
  1. package/dist/index.js +71 -180
  2. package/package.json +4 -6
  3. package/src/index.ts +91 -78
package/dist/index.js CHANGED
@@ -2,19 +2,41 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import { execSync } from "child_process";
6
5
  import { existsSync, readFileSync, writeFileSync, mkdirSync, watch } from "fs";
7
6
  import { homedir } from "os";
8
7
  import { join } from "path";
9
- var GLM_API_ENDPOINT = "https://api.z.ai/api/coding/paas/v4/chat/completions";
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");
8
+ import { GLMClient, GLMRateLimitError, GLMTimeoutError, GLMNetworkError } from "@ebowwa/ai";
9
+ function requireEnv(name) {
10
+ const value = process.env[name];
11
+ if (!value) {
12
+ throw new Error(`Missing required environment variable: ${name}`);
13
+ }
14
+ return value;
15
+ }
16
+ function requireEnvInt(name) {
17
+ return parseInt(requireEnv(name), 10);
18
+ }
19
+ function requireEnvFloat(name) {
20
+ return parseFloat(requireEnv(name));
21
+ }
22
+ var CONFIG = {
23
+ chatDir: process.env.SSH_CHAT_DIR ?? join(homedir(), ".ssh-chat"),
24
+ model: requireEnv("GLM_MODEL"),
25
+ maxRetries: requireEnvInt("GLM_MAX_RETRIES"),
26
+ timeout: requireEnvInt("GLM_TIMEOUT_MS"),
27
+ temperature: requireEnvFloat("GLM_TEMPERATURE"),
28
+ maxTokens: requireEnvInt("GLM_MAX_TOKENS"),
29
+ pollInterval: requireEnvInt("SSH_CHAT_POLL_MS"),
30
+ memoryLimit: requireEnvInt("SSH_CHAT_MEMORY_LIMIT"),
31
+ contextLimit: requireEnvInt("SSH_CHAT_CONTEXT_LIMIT")
32
+ };
33
+ var IN_FILE = join(CONFIG.chatDir, "in");
34
+ var OUT_FILE = join(CONFIG.chatDir, "out");
35
+ var STATUS_FILE = join(CONFIG.chatDir, "status");
36
+ var MEMORY_FILE = join(CONFIG.chatDir, "memory.json");
15
37
  function ensureDir() {
16
- if (!existsSync(CHAT_DIR)) {
17
- mkdirSync(CHAT_DIR, { recursive: true });
38
+ if (!existsSync(CONFIG.chatDir)) {
39
+ mkdirSync(CONFIG.chatDir, { recursive: true });
18
40
  }
19
41
  }
20
42
  function setStatus(status) {
@@ -31,7 +53,7 @@ class ConversationMemory {
31
53
  file;
32
54
  messages = [];
33
55
  maxMessages;
34
- constructor(file, maxMessages = 50) {
56
+ constructor(file, maxMessages = CONFIG.memoryLimit) {
35
57
  this.file = file;
36
58
  this.maxMessages = maxMessages;
37
59
  this.load();
@@ -68,180 +90,42 @@ class ConversationMemory {
68
90
  this.save();
69
91
  }
70
92
  }
71
- var TOOLS = [
72
- {
73
- name: "read_file",
74
- description: "Read a file from the filesystem.",
75
- parameters: {
76
- type: "object",
77
- properties: { path: { type: "string", description: "File path to read" } },
78
- required: ["path"]
79
- },
80
- handler: async (args) => {
81
- const path = args.path;
82
- try {
83
- if (!existsSync(path))
84
- return `File not found: ${path}`;
85
- const content = readFileSync(path, "utf-8");
86
- return content.length > 4000 ? content.slice(0, 4000) + `
87
- ...[truncated]` : content;
88
- } catch (e) {
89
- return `Error: ${e.message}`;
90
- }
91
- }
92
- },
93
- {
94
- name: "write_file",
95
- description: "Write content to a file.",
96
- parameters: {
97
- type: "object",
98
- properties: {
99
- path: { type: "string" },
100
- content: { type: "string" }
101
- },
102
- required: ["path", "content"]
103
- },
104
- handler: async (args) => {
105
- try {
106
- writeFileSync(args.path, args.content);
107
- return `Wrote ${args.content.length} bytes to ${args.path}`;
108
- } catch (e) {
109
- return `Error: ${e.message}`;
110
- }
111
- }
112
- },
113
- {
114
- name: "run_command",
115
- description: "Execute a shell command.",
116
- parameters: {
117
- type: "object",
118
- properties: {
119
- command: { type: "string" },
120
- cwd: { type: "string" }
121
- },
122
- required: ["command"]
123
- },
124
- handler: async (args) => {
125
- const cmd = args.command;
126
- const blocked = ["rm -rf", "mkfs", "dd if=", "> /dev/"];
127
- if (blocked.some((b) => cmd.includes(b)))
128
- return "Blocked: dangerous command";
129
- try {
130
- const result = execSync(cmd, { timeout: 1e4, cwd: args.cwd || process.cwd() });
131
- return result.toString() || "(no output)";
132
- } catch (e) {
133
- return e.stdout?.toString() || e.message;
134
- }
135
- }
136
- },
137
- {
138
- name: "git_status",
139
- description: "Check git repository status.",
140
- parameters: { type: "object", properties: { cwd: { type: "string" } } },
141
- handler: async (args) => {
142
- const cwd = args.cwd || process.cwd();
143
- try {
144
- const status = execSync("git status 2>&1", { cwd }).toString();
145
- const branch = execSync("git branch --show-current 2>&1", { cwd }).toString();
146
- return `Branch: ${branch}
147
-
148
- ${status}`;
149
- } catch (e) {
150
- return `Error: ${e.message}`;
151
- }
152
- }
153
- },
154
- {
155
- name: "system_info",
156
- description: "Get system resource info.",
157
- parameters: { type: "object", properties: {} },
158
- handler: async () => {
159
- try {
160
- const cpu = execSync('nproc 2>/dev/null || echo "unknown"').toString().trim();
161
- const mem = execSync('free -h 2>/dev/null | grep Mem || echo "unknown"').toString().trim();
162
- const disk = execSync('df -h / 2>/dev/null | tail -1 || echo "unknown"').toString().trim();
163
- return `CPU: ${cpu} cores
164
- Memory: ${mem}
165
- Disk: ${disk}`;
166
- } catch (e) {
167
- return `Error: ${e.message}`;
168
- }
169
- }
170
- }
171
- ];
172
- function getGLMTools() {
173
- return TOOLS.map((t) => ({
174
- type: "function",
175
- function: { name: t.name, description: t.description, parameters: t.parameters }
176
- }));
177
- }
178
- async function executeTool(name, args) {
179
- const tool = TOOLS.find((t) => t.name === name);
180
- if (tool)
181
- return tool.handler(args);
182
- return `Unknown tool: ${name}`;
183
- }
184
- function getAPIKey() {
185
- const envKey = process.env.ZAI_API_KEY || process.env.GLM_API_KEY;
186
- if (envKey)
187
- return envKey;
188
- const keysJson = process.env.ZAI_API_KEYS;
189
- if (keysJson) {
190
- try {
191
- const keys = JSON.parse(keysJson);
192
- if (Array.isArray(keys) && keys.length > 0) {
193
- return keys[Math.floor(Math.random() * keys.length)];
194
- }
195
- } catch {}
93
+ var glmClient = null;
94
+ function getClient() {
95
+ if (!glmClient) {
96
+ glmClient = new GLMClient;
196
97
  }
197
- throw new Error("No API key found. Set ZAI_API_KEY env var.");
98
+ return glmClient;
198
99
  }
199
100
  async function callGLM(messages) {
200
- const apiKey = getAPIKey();
201
- const response = await fetch(GLM_API_ENDPOINT, {
202
- method: "POST",
203
- headers: {
204
- "Content-Type": "application/json",
205
- Authorization: `Bearer ${apiKey}`
206
- },
207
- body: JSON.stringify({
208
- model: "glm-4-plus",
209
- messages: messages.map((m) => ({ role: m.role, content: m.content })),
210
- tools: getGLMTools(),
211
- temperature: 0.7,
212
- max_tokens: 4096
213
- })
214
- });
215
- if (!response.ok) {
216
- const text = await response.text();
217
- throw new Error(`GLM API error: ${response.status} - ${text}`);
218
- }
219
- const data = await response.json();
220
- const choice = data.choices?.[0];
221
- if (!choice) {
222
- throw new Error("No response from GLM");
223
- }
224
- if (choice.message?.tool_calls) {
225
- const toolResults = [];
226
- for (const tc of choice.message.tool_calls) {
227
- const toolName = tc.function?.name;
228
- const toolArgs = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
229
- const result = await executeTool(toolName, toolArgs);
230
- toolResults.push(`[${toolName}]: ${result}`);
101
+ const client = getClient();
102
+ try {
103
+ const response = await client.chatCompletion(messages.map((m) => ({ role: m.role, content: m.content })), {
104
+ model: CONFIG.model,
105
+ temperature: CONFIG.temperature,
106
+ maxTokens: CONFIG.maxTokens,
107
+ maxRetries: CONFIG.maxRetries,
108
+ timeout: CONFIG.timeout
109
+ });
110
+ return response.choices[0]?.message?.content || "(no response)";
111
+ } catch (error) {
112
+ if (error instanceof GLMRateLimitError) {
113
+ throw new Error(`Rate limit exceeded after ${CONFIG.maxRetries} retries. Please try again later or check API credits.`);
231
114
  }
232
- messages.push({ role: "assistant", content: "", timestamp: Date.now() });
233
- messages.push({ role: "user", content: `Tool results:
234
- ${toolResults.join(`
235
- `)}`, timestamp: Date.now() });
236
- return callGLM(messages);
115
+ if (error instanceof GLMTimeoutError) {
116
+ throw new Error(`Request timed out after ${CONFIG.maxRetries} retries.`);
117
+ }
118
+ if (error instanceof GLMNetworkError) {
119
+ throw new Error(`Network error after ${CONFIG.maxRetries} retries: ${error.message}`);
120
+ }
121
+ throw error;
237
122
  }
238
- return choice.message?.content || "(no response)";
239
123
  }
240
124
  async function processMessage(input, memory) {
241
125
  if (input.startsWith("/")) {
242
126
  if (input === "/clear") {
243
127
  memory.clear();
244
- return "\uD83D\uDDD1\uFE0F Memory cleared.";
128
+ return "Memory cleared.";
245
129
  }
246
130
  if (input === "/help") {
247
131
  return `Commands:
@@ -254,17 +138,17 @@ Just type a message to chat with AI.`;
254
138
  if (input === "/status") {
255
139
  return `Status: running
256
140
  Memory file: ${MEMORY_FILE}
257
- Chat dir: ${CHAT_DIR}`;
141
+ Chat dir: ${CONFIG.chatDir}`;
258
142
  }
259
143
  return `Unknown command: ${input}. Type /help for available commands.`;
260
144
  }
261
145
  memory.add("user", input);
262
- const messages = memory.getContext(20);
146
+ const messages = memory.getContext(CONFIG.contextLimit);
263
147
  return await callGLM(messages);
264
148
  }
265
149
  async function main() {
266
- console.log("\uD83E\uDD16 SSH Chat Channel starting...");
267
- console.log(`Chat dir: ${CHAT_DIR}`);
150
+ console.log("SSH Chat Channel starting...");
151
+ console.log(`Chat dir: ${CONFIG.chatDir}`);
268
152
  console.log(`Memory: ${MEMORY_FILE}`);
269
153
  console.log("");
270
154
  console.log("Usage:");
@@ -272,6 +156,13 @@ async function main() {
272
156
  console.log(` Read response: cat ${OUT_FILE}`);
273
157
  console.log("");
274
158
  ensureDir();
159
+ try {
160
+ getClient();
161
+ console.log("GLM client initialized with retry support");
162
+ } catch (e) {
163
+ console.error("Failed to initialize GLM client:", e.message);
164
+ process.exit(1);
165
+ }
275
166
  const memory = new ConversationMemory(MEMORY_FILE);
276
167
  memory.add("system", `You are an AI assistant accessible via SSH.
277
168
  You are helpful, concise, and can execute tools to help the user.
@@ -284,7 +175,7 @@ This is a private SSH channel separate from any Telegram or other chat interface
284
175
  setStatus("idle");
285
176
  let lastContent = "";
286
177
  console.log("Watching for messages...");
287
- const watcher = watch(CHAT_DIR, (eventType, filename) => {
178
+ const watcher = watch(CONFIG.chatDir, (eventType, filename) => {
288
179
  if (filename === "in" && eventType === "change") {
289
180
  processIncoming();
290
181
  }
@@ -316,7 +207,7 @@ This is a private SSH channel separate from any Telegram or other chat interface
316
207
  processIncoming();
317
208
  }
318
209
  } catch {}
319
- }, 500);
210
+ }, CONFIG.pollInterval);
320
211
  process.on("SIGINT", () => {
321
212
  console.log(`
322
213
  Shutting down...`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ebowwa/channel-ssh",
3
- "version": "1.0.2",
4
- "description": "SSH tmux channel for GLM Daemon - separate from Telegram",
3
+ "version": "1.1.0",
4
+ "description": "SSH chat channel for GLM AI - configurable via environment variables",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -9,13 +9,12 @@
9
9
  "channel-ssh": "./dist/index.js"
10
10
  },
11
11
  "scripts": {
12
- "build": "bun build src/index.ts --outdir dist --target bun --external '@ebowwa/*' --external 'node-telegram-bot-api'",
12
+ "build": "bun build src/index.ts --outdir dist --target bun --external '@ebowwa/*'",
13
13
  "dev": "bun run src/index.ts",
14
14
  "prepublishOnly": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@ebowwa/structured-prompts": "^0.3.2",
18
- "@ebowwa/terminal": "^0.3.0"
17
+ "@ebowwa/ai": "^0.1.0"
19
18
  },
20
19
  "devDependencies": {
21
20
  "@types/bun": "latest",
@@ -23,7 +22,6 @@
23
22
  },
24
23
  "keywords": [
25
24
  "ssh",
26
- "tmux",
27
25
  "channel",
28
26
  "glm",
29
27
  "ai"
package/src/index.ts CHANGED
@@ -15,34 +15,65 @@
15
15
  *
16
16
  * Features:
17
17
  * - File-based IPC for systemd compatibility
18
- * - GLM-4.7 AI responses
18
+ * - GLM-4.7 AI responses with retry logic (via @ebowwa/ai)
19
19
  * - Separate conversation memory from Telegram
20
20
  * - Tool support (read_file, run_command, etc.)
21
21
  */
22
22
 
23
23
  import { execSync } from 'child_process';
24
- import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, watch } from 'fs';
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, watch } from 'fs';
25
25
  import { homedir } from 'os';
26
- import { join, dirname } from 'path';
26
+ import { join } from 'path';
27
+ import { GLMClient, GLMRateLimitError, GLMTimeoutError, GLMNetworkError } from '@ebowwa/ai';
27
28
 
28
- const GLM_API_ENDPOINT = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
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');
29
+ // ====================================================================
30
+ // Configuration (all via environment variables - REQUIRED)
31
+ // ====================================================================
32
+
33
+ function requireEnv(name: string): string {
34
+ const value = process.env[name];
35
+ if (!value) {
36
+ throw new Error(`Missing required environment variable: ${name}`);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ function requireEnvInt(name: string): number {
42
+ return parseInt(requireEnv(name), 10);
43
+ }
44
+
45
+ function requireEnvFloat(name: string): number {
46
+ return parseFloat(requireEnv(name));
47
+ }
48
+
49
+ const CONFIG = {
50
+ chatDir: process.env.SSH_CHAT_DIR ?? join(homedir(), '.ssh-chat'), // only optional one
51
+ model: requireEnv('GLM_MODEL'),
52
+ maxRetries: requireEnvInt('GLM_MAX_RETRIES'),
53
+ timeout: requireEnvInt('GLM_TIMEOUT_MS'),
54
+ temperature: requireEnvFloat('GLM_TEMPERATURE'),
55
+ maxTokens: requireEnvInt('GLM_MAX_TOKENS'),
56
+ pollInterval: requireEnvInt('SSH_CHAT_POLL_MS'),
57
+ memoryLimit: requireEnvInt('SSH_CHAT_MEMORY_LIMIT'),
58
+ contextLimit: requireEnvInt('SSH_CHAT_CONTEXT_LIMIT'),
59
+ };
60
+
61
+ const IN_FILE = join(CONFIG.chatDir, 'in');
62
+ const OUT_FILE = join(CONFIG.chatDir, 'out');
63
+ const STATUS_FILE = join(CONFIG.chatDir, 'status');
64
+ const MEMORY_FILE = join(CONFIG.chatDir, 'memory.json');
34
65
 
35
66
  // ====================================================================
36
67
  // Setup
37
68
  // ====================================================================
38
69
 
39
70
  function ensureDir(): void {
40
- if (!existsSync(CHAT_DIR)) {
41
- mkdirSync(CHAT_DIR, { recursive: true });
71
+ if (!existsSync(CONFIG.chatDir)) {
72
+ mkdirSync(CONFIG.chatDir, { recursive: true });
42
73
  }
43
74
  }
44
75
 
45
- function setStatus(status: 'idle' | 'processing' | 'error'): void {
76
+ function setStatus(status: 'idle' | 'processing' | 'error' | 'retrying'): void {
46
77
  writeFileSync(STATUS_FILE, JSON.stringify({ status, timestamp: Date.now() }));
47
78
  }
48
79
 
@@ -65,7 +96,7 @@ class ConversationMemory {
65
96
  private messages: Message[] = [];
66
97
  private maxMessages: number;
67
98
 
68
- constructor(private file: string, maxMessages = 50) {
99
+ constructor(private file: string, maxMessages = CONFIG.memoryLimit) {
69
100
  this.maxMessages = maxMessages;
70
101
  this.load();
71
102
  }
@@ -227,74 +258,47 @@ async function executeTool(name: string, args: Record<string, unknown>): Promise
227
258
  }
228
259
 
229
260
  // ====================================================================
230
- // GLM API Client
261
+ // GLM API Client (using @ebowwa/ai with retry logic)
231
262
  // ====================================================================
232
263
 
233
- function getAPIKey(): string {
234
- const envKey = process.env.ZAI_API_KEY || process.env.GLM_API_KEY;
235
- if (envKey) return envKey;
264
+ let glmClient: GLMClient | null = null;
236
265
 
237
- const keysJson = process.env.ZAI_API_KEYS;
238
- if (keysJson) {
239
- try {
240
- const keys = JSON.parse(keysJson);
241
- if (Array.isArray(keys) && keys.length > 0) {
242
- return keys[Math.floor(Math.random() * keys.length)];
243
- }
244
- } catch {}
266
+ function getClient(): GLMClient {
267
+ if (!glmClient) {
268
+ glmClient = new GLMClient();
245
269
  }
246
-
247
- throw new Error('No API key found. Set ZAI_API_KEY env var.');
270
+ return glmClient;
248
271
  }
249
272
 
250
273
  async function callGLM(messages: Message[]): Promise<string> {
251
- const apiKey = getAPIKey();
252
-
253
- const response = await fetch(GLM_API_ENDPOINT, {
254
- method: 'POST',
255
- headers: {
256
- 'Content-Type': 'application/json',
257
- 'Authorization': `Bearer ${apiKey}`
258
- },
259
- body: JSON.stringify({
260
- model: 'glm-4-plus',
261
- messages: messages.map(m => ({ role: m.role, content: m.content })),
262
- tools: getGLMTools(),
263
- temperature: 0.7,
264
- max_tokens: 4096
265
- })
266
- });
267
-
268
- if (!response.ok) {
269
- const text = await response.text();
270
- throw new Error(`GLM API error: ${response.status} - ${text}`);
271
- }
272
-
273
- const data = await response.json();
274
- const choice = data.choices?.[0];
275
-
276
- if (!choice) {
277
- throw new Error('No response from GLM');
278
- }
279
-
280
- // Handle tool calls
281
- if (choice.message?.tool_calls) {
282
- const toolResults: string[] = [];
274
+ const client = getClient();
275
+
276
+ try {
277
+ const response = await client.chatCompletion(
278
+ messages.map(m => ({ role: m.role, content: m.content })),
279
+ {
280
+ model: CONFIG.model,
281
+ temperature: CONFIG.temperature,
282
+ maxTokens: CONFIG.maxTokens,
283
+ maxRetries: CONFIG.maxRetries,
284
+ timeout: CONFIG.timeout
285
+ }
286
+ );
283
287
 
284
- for (const tc of choice.message.tool_calls) {
285
- const toolName = tc.function?.name;
286
- const toolArgs = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
287
- const result = await executeTool(toolName, toolArgs);
288
- toolResults.push(`[${toolName}]: ${result}`);
288
+ return response.choices[0]?.message?.content || '(no response)';
289
+ } catch (error) {
290
+ // Provide better error messages based on error type
291
+ if (error instanceof GLMRateLimitError) {
292
+ throw new Error(`Rate limit exceeded after ${CONFIG.maxRetries} retries. Please try again later or check API credits.`);
289
293
  }
290
-
291
- messages.push({ role: 'assistant', content: '', timestamp: Date.now() });
292
- messages.push({ role: 'user', content: `Tool results:\n${toolResults.join('\n')}`, timestamp: Date.now() });
293
-
294
- return callGLM(messages);
294
+ if (error instanceof GLMTimeoutError) {
295
+ throw new Error(`Request timed out after ${CONFIG.maxRetries} retries.`);
296
+ }
297
+ if (error instanceof GLMNetworkError) {
298
+ throw new Error(`Network error after ${CONFIG.maxRetries} retries: ${error.message}`);
299
+ }
300
+ throw error;
295
301
  }
296
-
297
- return choice.message?.content || '(no response)';
298
302
  }
299
303
 
300
304
  // ====================================================================
@@ -306,7 +310,7 @@ async function processMessage(input: string, memory: ConversationMemory): Promis
306
310
  if (input.startsWith('/')) {
307
311
  if (input === '/clear') {
308
312
  memory.clear();
309
- return '🗑️ Memory cleared.';
313
+ return 'Memory cleared.';
310
314
  }
311
315
  if (input === '/help') {
312
316
  return `Commands:
@@ -319,14 +323,14 @@ Just type a message to chat with AI.`;
319
323
  if (input === '/status') {
320
324
  return `Status: running
321
325
  Memory file: ${MEMORY_FILE}
322
- Chat dir: ${CHAT_DIR}`;
326
+ Chat dir: ${CONFIG.chatDir}`;
323
327
  }
324
328
  return `Unknown command: ${input}. Type /help for available commands.`;
325
329
  }
326
330
 
327
331
  // Regular message - get AI response
328
332
  memory.add('user', input);
329
- const messages = memory.getContext(20);
333
+ const messages = memory.getContext(CONFIG.contextLimit);
330
334
  return await callGLM(messages);
331
335
  }
332
336
 
@@ -335,8 +339,8 @@ Chat dir: ${CHAT_DIR}`;
335
339
  // ====================================================================
336
340
 
337
341
  async function main() {
338
- console.log('🤖 SSH Chat Channel starting...');
339
- console.log(`Chat dir: ${CHAT_DIR}`);
342
+ console.log('SSH Chat Channel starting...');
343
+ console.log(`Chat dir: ${CONFIG.chatDir}`);
340
344
  console.log(`Memory: ${MEMORY_FILE}`);
341
345
  console.log('');
342
346
  console.log('Usage:');
@@ -347,6 +351,15 @@ async function main() {
347
351
  // Ensure directories exist
348
352
  ensureDir();
349
353
 
354
+ // Initialize GLM client (will throw if no API key)
355
+ try {
356
+ getClient();
357
+ console.log('GLM client initialized with retry support');
358
+ } catch (e) {
359
+ console.error('Failed to initialize GLM client:', (e as Error).message);
360
+ process.exit(1);
361
+ }
362
+
350
363
  // Initialize memory
351
364
  const memory = new ConversationMemory(MEMORY_FILE);
352
365
  memory.add('system', `You are an AI assistant accessible via SSH.
@@ -365,7 +378,7 @@ This is a private SSH channel separate from any Telegram or other chat interface
365
378
  console.log('Watching for messages...');
366
379
 
367
380
  // Watch for file changes
368
- const watcher = watch(CHAT_DIR, (eventType, filename) => {
381
+ const watcher = watch(CONFIG.chatDir, (eventType, filename) => {
369
382
  if (filename === 'in' && eventType === 'change') {
370
383
  processIncoming();
371
384
  }
@@ -410,7 +423,7 @@ This is a private SSH channel separate from any Telegram or other chat interface
410
423
  processIncoming();
411
424
  }
412
425
  } catch {}
413
- }, 500);
426
+ }, CONFIG.pollInterval);
414
427
 
415
428
  // Keep running
416
429
  process.on('SIGINT', () => {