@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.
- package/dist/index.js +71 -180
- package/package.json +4 -6
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
17
|
-
mkdirSync(
|
|
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 =
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
98
|
+
return glmClient;
|
|
198
99
|
}
|
|
199
100
|
async function callGLM(messages) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 "
|
|
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: ${
|
|
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(
|
|
146
|
+
const messages = memory.getContext(CONFIG.contextLimit);
|
|
263
147
|
return await callGLM(messages);
|
|
264
148
|
}
|
|
265
149
|
async function main() {
|
|
266
|
-
console.log("
|
|
267
|
-
console.log(`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(
|
|
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
|
-
},
|
|
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
|
|
4
|
-
"description": "SSH
|
|
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/*'
|
|
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/
|
|
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,
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, watch } from 'fs';
|
|
25
25
|
import { homedir } from 'os';
|
|
26
|
-
import { join
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
import { GLMClient, GLMRateLimitError, GLMTimeoutError, GLMNetworkError } from '@ebowwa/ai';
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
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(
|
|
41
|
-
mkdirSync(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 '
|
|
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: ${
|
|
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(
|
|
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('
|
|
339
|
-
console.log(`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(
|
|
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
|
-
},
|
|
426
|
+
}, CONFIG.pollInterval);
|
|
414
427
|
|
|
415
428
|
// Keep running
|
|
416
429
|
process.on('SIGINT', () => {
|