@ebowwa/channel-ssh 1.1.3 → 2.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 CHANGED
@@ -1,386 +1,346 @@
1
- #!/usr/bin/env bun
2
- // @bun
3
-
4
- // src/index.ts
5
- import { execSync } from "child_process";
6
- import { existsSync, readFileSync, writeFileSync, mkdirSync, watch } from "fs";
1
+ /**
2
+ * @ebowwa/channel-ssh
3
+ *
4
+ * SSH channel adapter implementing ChannelConnector.
5
+ *
6
+ * This package handles:
7
+ * - File-based IPC (reads from ~/.ssh-chat/in, writes to ~/.ssh-chat/out)
8
+ * - Status tracking (~/.ssh-chat/status)
9
+ * - Message normalization to ChannelMessage format
10
+ *
11
+ * Intelligence (GLM, tools, memory) can be provided by:
12
+ * 1. This package (standalone mode with built-in echo handler)
13
+ * 2. External daemon/consumer (adapter mode via onMessage handler)
14
+ *
15
+ * Usage:
16
+ * Write message: echo "your message" > ~/.ssh-chat/in
17
+ * Read response: cat ~/.ssh-chat/out
18
+ * Check status: cat ~/.ssh-chat/status
19
+ */
20
+ import { existsSync, readFileSync, watch, writeFileSync, mkdirSync } from "fs";
7
21
  import { homedir } from "os";
8
22
  import { join } from "path";
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;
23
+ import { createChannelId, } from "@ebowwa/channel-types";
24
+ export function createSSHConfigFromEnv() {
25
+ return {
26
+ chatDir: process.env.SSH_CHAT_DIR,
27
+ pollInterval: process.env.SSH_POLL_INTERVAL
28
+ ? parseInt(process.env.SSH_POLL_INTERVAL, 10)
29
+ : undefined,
30
+ memoryLimit: process.env.SSH_MEMORY_LIMIT
31
+ ? parseInt(process.env.SSH_MEMORY_LIMIT, 10)
32
+ : undefined,
33
+ contextLimit: process.env.SSH_CONTEXT_LIMIT
34
+ ? parseInt(process.env.SSH_CONTEXT_LIMIT, 10)
35
+ : undefined,
36
+ };
15
37
  }
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");
37
- function ensureDir() {
38
- if (!existsSync(CONFIG.chatDir)) {
39
- mkdirSync(CONFIG.chatDir, { recursive: true });
40
- }
41
- }
42
- function setStatus(status) {
43
- writeFileSync(STATUS_FILE, JSON.stringify({ status, timestamp: Date.now() }));
44
- }
45
- function writeOutput(text) {
46
- const timestamp = new Date().toISOString();
47
- writeFileSync(OUT_FILE, `[${timestamp}]
48
- ${text}
49
- `);
50
- }
51
-
52
- class ConversationMemory {
53
- file;
54
- messages = [];
55
- maxMessages;
56
- constructor(file, maxMessages = CONFIG.memoryLimit) {
57
- this.file = file;
58
- this.maxMessages = maxMessages;
59
- this.load();
60
- }
61
- load() {
62
- try {
63
- if (existsSync(this.file)) {
64
- const data = JSON.parse(readFileSync(this.file, "utf-8"));
65
- this.messages = data.messages || [];
66
- }
67
- } catch {
68
- this.messages = [];
38
+ export class ConversationMemory {
39
+ messages = [];
40
+ maxMessages;
41
+ file;
42
+ constructor(file, maxMessages = 100) {
43
+ this.file = file;
44
+ this.maxMessages = maxMessages;
45
+ this.load();
46
+ }
47
+ load() {
48
+ try {
49
+ if (existsSync(this.file)) {
50
+ const data = JSON.parse(readFileSync(this.file, "utf-8"));
51
+ this.messages = data.messages || [];
52
+ }
53
+ }
54
+ catch {
55
+ this.messages = [];
56
+ }
69
57
  }
70
- }
71
- save() {
72
- try {
73
- writeFileSync(this.file, JSON.stringify({ messages: this.messages }, null, 2));
74
- } catch (e) {
75
- console.error("Failed to save memory:", e);
58
+ save() {
59
+ try {
60
+ writeFileSync(this.file, JSON.stringify({ messages: this.messages }, null, 2));
61
+ }
62
+ catch (e) {
63
+ console.error("Failed to save memory:", e);
64
+ }
76
65
  }
77
- }
78
- add(role, content) {
79
- this.messages.push({ role, content, timestamp: Date.now() });
80
- if (this.messages.length > this.maxMessages) {
81
- this.messages = this.messages.slice(-this.maxMessages);
66
+ add(role, content) {
67
+ this.messages.push({ role, content, timestamp: Date.now() });
68
+ if (this.messages.length > this.maxMessages) {
69
+ this.messages = this.messages.slice(-this.maxMessages);
70
+ }
71
+ this.save();
72
+ }
73
+ getContext(limit = 20) {
74
+ return this.messages.slice(-limit);
75
+ }
76
+ clear() {
77
+ this.messages = [];
78
+ this.save();
82
79
  }
83
- this.save();
84
- }
85
- getContext(limit = 20) {
86
- return this.messages.slice(-limit);
87
- }
88
- clear() {
89
- this.messages = [];
90
- this.save();
91
- }
92
80
  }
93
- var TOOLS = [
94
- {
95
- name: "read_file",
96
- description: "Read a file from the filesystem.",
97
- parameters: {
98
- type: "object",
99
- properties: { path: { type: "string", description: "File path to read" } },
100
- required: ["path"]
101
- },
102
- handler: async (args) => {
103
- const path = args.path;
104
- try {
105
- if (!existsSync(path))
106
- return `File not found: ${path}`;
107
- const content = readFileSync(path, "utf-8");
108
- return content.length > 4000 ? content.slice(0, 4000) + `
109
- ...[truncated]` : content;
110
- } catch (e) {
111
- return `Error: ${e.message}`;
112
- }
81
+ // ============================================================
82
+ // SSH CHANNEL
83
+ // ============================================================
84
+ export class SSHChannel {
85
+ id;
86
+ label = "SSH";
87
+ capabilities = {
88
+ supports: {
89
+ text: true,
90
+ media: false,
91
+ replies: false,
92
+ threads: false,
93
+ reactions: false,
94
+ editing: false,
95
+ streaming: false,
96
+ },
97
+ rateLimits: {
98
+ messagesPerMinute: 120,
99
+ charactersPerMessage: 100000,
100
+ },
101
+ };
102
+ config;
103
+ memory;
104
+ messageHandler;
105
+ connected = false;
106
+ watcher;
107
+ pollInterval;
108
+ lastContent = "";
109
+ messageCounter = 0;
110
+ // File paths
111
+ IN_FILE;
112
+ OUT_FILE;
113
+ STATUS_FILE;
114
+ MEMORY_FILE;
115
+ constructor(config = {}) {
116
+ this.config = {
117
+ chatDir: config.chatDir ?? join(homedir(), ".ssh-chat"),
118
+ pollInterval: config.pollInterval ?? 500,
119
+ memoryLimit: config.memoryLimit ?? 100,
120
+ contextLimit: config.contextLimit ?? 20,
121
+ };
122
+ this.id = createChannelId("ssh", "default");
123
+ this.IN_FILE = join(this.config.chatDir, "in");
124
+ this.OUT_FILE = join(this.config.chatDir, "out");
125
+ this.STATUS_FILE = join(this.config.chatDir, "status");
126
+ this.MEMORY_FILE = join(this.config.chatDir, "memory.json");
127
+ this.memory = new ConversationMemory(this.MEMORY_FILE, this.config.memoryLimit);
113
128
  }
114
- },
115
- {
116
- name: "write_file",
117
- description: "Write content to a file.",
118
- parameters: {
119
- type: "object",
120
- properties: {
121
- path: { type: "string" },
122
- content: { type: "string" }
123
- },
124
- required: ["path", "content"]
125
- },
126
- handler: async (args) => {
127
- try {
128
- writeFileSync(args.path, args.content);
129
- return `Wrote ${args.content.length} bytes to ${args.path}`;
130
- } catch (e) {
131
- return `Error: ${e.message}`;
132
- }
129
+ // ============================================================
130
+ // ChannelConnector Implementation
131
+ // ============================================================
132
+ async start() {
133
+ console.log("[SSHChannel] Starting...");
134
+ // Ensure directory exists
135
+ this.ensureDir();
136
+ // Create empty files if they don't exist
137
+ if (!existsSync(this.IN_FILE))
138
+ writeFileSync(this.IN_FILE, "");
139
+ if (!existsSync(this.OUT_FILE))
140
+ writeFileSync(this.OUT_FILE, "Ready. Send a message.\n");
141
+ this.setStatus("idle");
142
+ // Setup file watcher
143
+ this.watcher = watch(this.config.chatDir, (eventType, filename) => {
144
+ if (filename === "in" && eventType === "change") {
145
+ this.processIncoming();
146
+ }
147
+ });
148
+ // Setup polling as backup (watch can be unreliable)
149
+ this.pollInterval = setInterval(() => {
150
+ try {
151
+ const content = readFileSync(this.IN_FILE, "utf-8").trim();
152
+ if (content && content !== this.lastContent) {
153
+ this.processIncoming();
154
+ }
155
+ }
156
+ catch { }
157
+ }, this.config.pollInterval);
158
+ this.connected = true;
159
+ console.log("[SSHChannel] Started");
160
+ console.log(` IN_FILE: ${this.IN_FILE}`);
161
+ console.log(` OUT_FILE: ${this.OUT_FILE}`);
162
+ console.log(` STATUS: ${this.STATUS_FILE}`);
133
163
  }
134
- },
135
- {
136
- name: "run_command",
137
- description: "Execute a shell command.",
138
- parameters: {
139
- type: "object",
140
- properties: {
141
- command: { type: "string" },
142
- cwd: { type: "string" }
143
- },
144
- required: ["command"]
145
- },
146
- handler: async (args) => {
147
- const cmd = args.command;
148
- const blocked = ["rm -rf", "mkfs", "dd if=", "> /dev/"];
149
- if (blocked.some((b) => cmd.includes(b)))
150
- return "Blocked: dangerous command";
151
- try {
152
- const result = execSync(cmd, { timeout: 1e4, cwd: args.cwd || process.cwd() });
153
- return result.toString() || "(no output)";
154
- } catch (e) {
155
- return e.stdout?.toString() || e.message;
156
- }
164
+ async stop() {
165
+ console.log("[SSHChannel] Stopping...");
166
+ if (this.watcher) {
167
+ this.watcher.close();
168
+ this.watcher = undefined;
169
+ }
170
+ if (this.pollInterval) {
171
+ clearInterval(this.pollInterval);
172
+ this.pollInterval = undefined;
173
+ }
174
+ this.connected = false;
175
+ console.log("[SSHChannel] Stopped");
157
176
  }
158
- },
159
- {
160
- name: "git_status",
161
- description: "Check git repository status.",
162
- parameters: { type: "object", properties: { cwd: { type: "string" } } },
163
- handler: async (args) => {
164
- const cwd = args.cwd || process.cwd();
165
- try {
166
- const status = execSync("git status 2>&1", { cwd }).toString();
167
- const branch = execSync("git branch --show-current 2>&1", { cwd }).toString();
168
- return `Branch: ${branch}
169
-
170
- ${status}`;
171
- } catch (e) {
172
- return `Error: ${e.message}`;
173
- }
177
+ /**
178
+ * Set the message handler. The daemon/consumer provides intelligence.
179
+ */
180
+ onMessage(handler) {
181
+ this.messageHandler = handler;
174
182
  }
175
- },
176
- {
177
- name: "system_info",
178
- description: "Get system resource info.",
179
- parameters: { type: "object", properties: {} },
180
- handler: async () => {
181
- try {
182
- const cpu = execSync('nproc 2>/dev/null || echo "unknown"').toString().trim();
183
- const mem = execSync('free -h 2>/dev/null | grep Mem || echo "unknown"').toString().trim();
184
- const disk = execSync('df -h / 2>/dev/null | tail -1 || echo "unknown"').toString().trim();
185
- return `CPU: ${cpu} cores
186
- Memory: ${mem}
187
- Disk: ${disk}`;
188
- } catch (e) {
189
- return `Error: ${e.message}`;
190
- }
183
+ /**
184
+ * Send a response back to SSH channel (writes to OUT_FILE).
185
+ */
186
+ async send(response) {
187
+ const text = response.content.text;
188
+ const timestamp = new Date().toISOString();
189
+ writeFileSync(this.OUT_FILE, `[${timestamp}]\n${text}\n`);
190
+ // Store in memory
191
+ this.memory.add("assistant", text);
191
192
  }
192
- }
193
- ];
194
- function getGLMTools() {
195
- return TOOLS.map((t) => ({
196
- type: "function",
197
- function: { name: t.name, description: t.description, parameters: t.parameters }
198
- }));
199
- }
200
- async function executeTool(name, args) {
201
- const tool = TOOLS.find((t) => t.name === name);
202
- if (tool)
203
- return tool.handler(args);
204
- return `Unknown tool: ${name}`;
205
- }
206
- var GLM_API_ENDPOINT = "https://api.z.ai/api/coding/paas/v4/chat/completions";
207
- function getAPIKey() {
208
- const envKey = process.env.ZAI_API_KEY || process.env.Z_AI_API_KEY || process.env.GLM_API_KEY;
209
- if (envKey)
210
- return envKey;
211
- const keysJson = process.env.ZAI_API_KEYS || process.env.Z_AI_API_KEYS;
212
- if (keysJson) {
213
- try {
214
- const keys = JSON.parse(keysJson);
215
- if (Array.isArray(keys) && keys.length > 0) {
216
- return keys[Math.floor(Math.random() * keys.length)];
217
- }
218
- } catch {}
219
- }
220
- throw new Error("No API key found. Set ZAI_API_KEY env var.");
221
- }
222
- function sleep(ms) {
223
- return new Promise((resolve) => setTimeout(resolve, ms));
224
- }
225
- function calculateBackoff(retryCount) {
226
- return Math.min(1000 * Math.pow(2, retryCount), 1e4);
227
- }
228
- async function callGLM(messages, retryCount = 0) {
229
- const apiKey = getAPIKey();
230
- try {
231
- const controller = new AbortController;
232
- const timeoutId = setTimeout(() => controller.abort(), CONFIG.timeout);
233
- const response = await fetch(GLM_API_ENDPOINT, {
234
- method: "POST",
235
- headers: {
236
- "Content-Type": "application/json",
237
- Authorization: `Bearer ${apiKey}`
238
- },
239
- signal: controller.signal,
240
- body: JSON.stringify({
241
- model: CONFIG.model,
242
- messages: messages.map((m) => ({ role: m.role, content: m.content })),
243
- tools: getGLMTools(),
244
- temperature: CONFIG.temperature,
245
- max_tokens: CONFIG.maxTokens
246
- })
247
- });
248
- clearTimeout(timeoutId);
249
- if (!response.ok) {
250
- const text = await response.text();
251
- if ((response.status === 429 || response.status >= 500) && retryCount < CONFIG.maxRetries) {
252
- const backoff = calculateBackoff(retryCount);
253
- console.log(`GLM API error ${response.status}, retrying in ${backoff}ms (${retryCount + 1}/${CONFIG.maxRetries})`);
254
- await sleep(backoff);
255
- return callGLM(messages, retryCount + 1);
256
- }
257
- throw new Error(`GLM API error: ${response.status} - ${text}`);
193
+ isConnected() {
194
+ return this.connected;
258
195
  }
259
- const data = await response.json();
260
- const choice = data.choices?.[0];
261
- if (!choice) {
262
- throw new Error("No response from GLM");
196
+ // ============================================================
197
+ // Public Helpers
198
+ // ============================================================
199
+ /**
200
+ * Get the conversation memory.
201
+ */
202
+ getMemory() {
203
+ return this.memory;
263
204
  }
264
- if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
265
- const toolResults = [];
266
- for (const tc of choice.message.tool_calls) {
267
- const toolName = tc.function?.name;
268
- const toolArgs = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
269
- const result = await executeTool(toolName, toolArgs);
270
- toolResults.push(`[${toolName}]: ${result}`);
271
- }
272
- const updatedMessages = [
273
- ...messages,
274
- { role: "assistant", content: choice.message.content || "", timestamp: Date.now() },
275
- { role: "user", content: `Tool results:
276
- ${toolResults.join(`
277
- `)}`, timestamp: Date.now() }
278
- ];
279
- return callGLM(updatedMessages, 0);
205
+ /**
206
+ * Get the input file path.
207
+ */
208
+ getInFile() {
209
+ return this.IN_FILE;
280
210
  }
281
- return choice.message?.content || "(no response)";
282
- } catch (error) {
283
- if (error instanceof Error && (error.name === "AbortError" || error.message.includes("fetch")) && retryCount < CONFIG.maxRetries) {
284
- const backoff = calculateBackoff(retryCount);
285
- console.log(`Network error, retrying in ${backoff}ms (${retryCount + 1}/${CONFIG.maxRetries})`);
286
- await sleep(backoff);
287
- return callGLM(messages, retryCount + 1);
211
+ /**
212
+ * Get the output file path.
213
+ */
214
+ getOutFile() {
215
+ return this.OUT_FILE;
288
216
  }
289
- throw error;
290
- }
291
- }
292
- async function processMessage(input, memory) {
293
- if (input.startsWith("/")) {
294
- if (input === "/clear") {
295
- memory.clear();
296
- return "Memory cleared.";
217
+ /**
218
+ * Get the status file path.
219
+ */
220
+ getStatusFile() {
221
+ return this.STATUS_FILE;
297
222
  }
298
- if (input === "/help") {
299
- return `Commands:
300
- /clear - Clear conversation memory
301
- /help - Show this help
302
- /status - Show system status
303
-
304
- Just type a message to chat with AI.`;
223
+ // ============================================================
224
+ // Internal Methods
225
+ // ============================================================
226
+ ensureDir() {
227
+ if (!existsSync(this.config.chatDir)) {
228
+ mkdirSync(this.config.chatDir, { recursive: true });
229
+ }
305
230
  }
306
- if (input === "/status") {
307
- return `Status: running
308
- Memory file: ${MEMORY_FILE}
309
- Chat dir: ${CONFIG.chatDir}`;
231
+ setStatus(status) {
232
+ writeFileSync(this.STATUS_FILE, JSON.stringify({ status, timestamp: Date.now() }));
310
233
  }
311
- return `Unknown command: ${input}. Type /help for available commands.`;
312
- }
313
- memory.add("user", input);
314
- const messages = memory.getContext(CONFIG.contextLimit);
315
- return await callGLM(messages);
316
- }
317
- async function main() {
318
- console.log("SSH Chat Channel starting...");
319
- console.log(`Chat dir: ${CONFIG.chatDir}`);
320
- console.log(`Memory: ${MEMORY_FILE}`);
321
- console.log("");
322
- console.log("Usage:");
323
- console.log(` Write message: echo "your message" > ${IN_FILE}`);
324
- console.log(` Read response: cat ${OUT_FILE}`);
325
- console.log("");
326
- ensureDir();
327
- try {
328
- getAPIKey();
329
- console.log("GLM client initialized with retry support");
330
- } catch (e) {
331
- console.error("Failed to initialize GLM client:", e.message);
332
- process.exit(1);
333
- }
334
- const memory = new ConversationMemory(MEMORY_FILE);
335
- memory.add("system", `You are an AI assistant accessible via SSH.
336
- You are helpful, concise, and can execute tools to help the user.
337
- This is a private SSH channel separate from any Telegram or other chat interfaces.`);
338
- if (!existsSync(IN_FILE))
339
- writeFileSync(IN_FILE, "");
340
- if (!existsSync(OUT_FILE))
341
- writeFileSync(OUT_FILE, `Ready. Send a message.
342
- `);
343
- setStatus("idle");
344
- let lastContent = "";
345
- console.log("Watching for messages...");
346
- const watcher = watch(CONFIG.chatDir, (eventType, filename) => {
347
- if (filename === "in" && eventType === "change") {
348
- processIncoming();
234
+ async processIncoming() {
235
+ try {
236
+ const content = readFileSync(this.IN_FILE, "utf-8").trim();
237
+ // Skip if same as last or empty
238
+ if (!content || content === this.lastContent)
239
+ return;
240
+ this.lastContent = content;
241
+ this.setStatus("processing");
242
+ console.log(`[SSHChannel] Processing: ${content.slice(0, 50)}...`);
243
+ // Clear input file after reading
244
+ writeFileSync(this.IN_FILE, "");
245
+ // Create normalized message
246
+ const message = this.createChannelMessage(content);
247
+ // Store in memory
248
+ this.memory.add("user", content);
249
+ // Route to handler if set
250
+ if (this.messageHandler) {
251
+ try {
252
+ const response = await this.messageHandler(message);
253
+ if (response) {
254
+ await this.send(response);
255
+ }
256
+ }
257
+ catch (error) {
258
+ console.error("[SSHChannel] Handler error:", error);
259
+ writeFileSync(this.OUT_FILE, `[${new Date().toISOString()}]\nError: ${error.message}\n`);
260
+ this.setStatus("error");
261
+ return;
262
+ }
263
+ }
264
+ else {
265
+ // No handler - echo mode
266
+ await this.send({
267
+ content: { text: `Echo: ${content}` },
268
+ replyTo: { messageId: message.messageId, channelId: message.channelId },
269
+ });
270
+ }
271
+ this.setStatus("idle");
272
+ console.log("[SSHChannel] Response sent");
273
+ }
274
+ catch (error) {
275
+ console.error("[SSHChannel] Error:", error);
276
+ this.setStatus("error");
277
+ }
349
278
  }
350
- });
351
- async function processIncoming() {
352
- try {
353
- const content = readFileSync(IN_FILE, "utf-8").trim();
354
- if (!content || content === lastContent)
355
- return;
356
- lastContent = content;
357
- setStatus("processing");
358
- console.log(`[${new Date().toISOString()}] Processing: ${content.slice(0, 50)}...`);
359
- writeFileSync(IN_FILE, "");
360
- const response = await processMessage(content, memory);
361
- writeOutput(response);
362
- memory.add("assistant", response);
363
- setStatus("idle");
364
- console.log(`[${new Date().toISOString()}] Response sent`);
365
- } catch (error) {
366
- console.error("Error:", error);
367
- setStatus("error");
368
- writeOutput(`Error: ${error.message}`);
279
+ /**
280
+ * Normalize SSH input to ChannelMessage format.
281
+ */
282
+ createChannelMessage(text) {
283
+ this.messageCounter++;
284
+ const messageId = `ssh-${Date.now()}-${this.messageCounter}`;
285
+ const sender = {
286
+ id: "ssh-user",
287
+ username: "ssh",
288
+ displayName: "SSH User",
289
+ isBot: false,
290
+ };
291
+ const context = {
292
+ isDM: true,
293
+ metadata: {
294
+ source: "file-ipc",
295
+ chatDir: this.config.chatDir,
296
+ },
297
+ };
298
+ return {
299
+ messageId,
300
+ channelId: this.id,
301
+ timestamp: new Date(),
302
+ sender,
303
+ text,
304
+ context,
305
+ };
369
306
  }
370
- }
371
- setInterval(() => {
372
- try {
373
- const content = readFileSync(IN_FILE, "utf-8").trim();
374
- if (content && content !== lastContent) {
375
- processIncoming();
376
- }
377
- } catch {}
378
- }, CONFIG.pollInterval);
379
- process.on("SIGINT", () => {
380
- console.log(`
381
- Shutting down...`);
382
- watcher.close();
383
- process.exit(0);
384
- });
385
307
  }
386
- main().catch(console.error);
308
+ // ============================================================
309
+ // FACTORY
310
+ // ============================================================
311
+ export function createSSHChannel(config) {
312
+ return new SSHChannel(config);
313
+ }
314
+ // ============================================================
315
+ // MAIN ENTRY POINT (for standalone testing)
316
+ // ============================================================
317
+ async function main() {
318
+ const config = createSSHConfigFromEnv();
319
+ const channel = createSSHChannel(config);
320
+ // Example: Echo handler for testing
321
+ channel.onMessage(async (msg) => {
322
+ console.log(`Received: ${msg.text}`);
323
+ return {
324
+ content: { text: `Echo: ${msg.text}` },
325
+ replyTo: { messageId: msg.messageId, channelId: msg.channelId },
326
+ };
327
+ });
328
+ process.on("SIGINT", async () => {
329
+ await channel.stop();
330
+ process.exit(0);
331
+ });
332
+ process.on("SIGTERM", async () => {
333
+ await channel.stop();
334
+ process.exit(0);
335
+ });
336
+ await channel.start();
337
+ }
338
+ // Only run main when executed directly (not when imported)
339
+ if (import.meta.main) {
340
+ main().catch(console.error);
341
+ }
342
+ // ============================================================
343
+ // PLUGIN SYSTEM
344
+ // ============================================================
345
+ export { createSSHPlugin, sshPlugin } from "./plugin.js";
346
+ //# sourceMappingURL=index.js.map