@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.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +324 -364
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +136 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +29 -7
- package/src/index.ts +333 -398
- package/src/plugin.ts +179 -0
package/dist/index.js
CHANGED
|
@@ -1,386 +1,346 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
196
|
+
// ============================================================
|
|
197
|
+
// Public Helpers
|
|
198
|
+
// ============================================================
|
|
199
|
+
/**
|
|
200
|
+
* Get the conversation memory.
|
|
201
|
+
*/
|
|
202
|
+
getMemory() {
|
|
203
|
+
return this.memory;
|
|
263
204
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
223
|
+
// ============================================================
|
|
224
|
+
// Internal Methods
|
|
225
|
+
// ============================================================
|
|
226
|
+
ensureDir() {
|
|
227
|
+
if (!existsSync(this.config.chatDir)) {
|
|
228
|
+
mkdirSync(this.config.chatDir, { recursive: true });
|
|
229
|
+
}
|
|
305
230
|
}
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|