@ebowwa/stack 0.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/README.md +38 -0
- package/dist/index.js +57684 -0
- package/package.json +51 -0
- package/src/index.ts +467 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ebowwa/stack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Full-stack daemon orchestrator combining unified-router (cross-channel) and node-agent (Ralph orchestration)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"stack": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "bun --hot run src/index.ts",
|
|
18
|
+
"start": "bun run src/index.ts",
|
|
19
|
+
"build": "bun build src/index.ts --target bun --outdir ./dist"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"stack",
|
|
23
|
+
"orchestrator",
|
|
24
|
+
"daemon",
|
|
25
|
+
"ralph",
|
|
26
|
+
"telegram",
|
|
27
|
+
"ssh",
|
|
28
|
+
"ai"
|
|
29
|
+
],
|
|
30
|
+
"author": "Ebowwa Labs <labs@ebowwa.com>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/ebowwa/codespaces.git",
|
|
35
|
+
"directory": "packages/src/stack"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@ebowwa/ai": "^0.3.3",
|
|
39
|
+
"@ebowwa/channel-core": "^1.3.0",
|
|
40
|
+
"@ebowwa/channel-ssh": "^2.1.1",
|
|
41
|
+
"@ebowwa/channel-telegram": "^1.14.2",
|
|
42
|
+
"@ebowwa/channel-types": "^0.2.1",
|
|
43
|
+
"@ebowwa/codespaces-types": "^1.6.1",
|
|
44
|
+
"@ebowwa/daemons": "^0.5.0",
|
|
45
|
+
"@ebowwa/node-agent": "^0.6.4",
|
|
46
|
+
"@ebowwa/rolling-keys": "^0.1.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/bun": "latest"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* @ebowwa/stack - Full-Stack Daemon Orchestrator
|
|
4
|
+
*
|
|
5
|
+
* Combines:
|
|
6
|
+
* - Unified Router: Cross-channel communication (SSH + Telegram)
|
|
7
|
+
* - Node Agent: Ralph loop orchestration, worktrees, monitoring
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* ┌──────────────────────────────────────────────────────────────┐
|
|
11
|
+
* │ STACK │
|
|
12
|
+
* │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
|
13
|
+
* │ │ Unified Router │ │ Node Agent │ │ HTTP API │ │
|
|
14
|
+
* │ │ (chat/messaging)│ │ (Ralph loops) │ │ (:8911) │ │
|
|
15
|
+
* │ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │
|
|
16
|
+
* │ │ │ │ │
|
|
17
|
+
* │ └───────────────────┴───────────────────┘ │
|
|
18
|
+
* │ │ │
|
|
19
|
+
* │ ┌─────────▼─────────┐ │
|
|
20
|
+
* │ │ Shared Memory │ │
|
|
21
|
+
* │ │ (Cross-Context) │ │
|
|
22
|
+
* │ └───────────────────┘ │
|
|
23
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createChannelRouter, createPermissionMemory, parseMemoryCommand } from "@ebowwa/channel-core";
|
|
27
|
+
import type { ChannelConnector, ChannelMessage, ChannelResponse, ChannelId } from "@ebowwa/channel-types";
|
|
28
|
+
import { GLMClient } from "@ebowwa/ai";
|
|
29
|
+
import { ToolExecutor, BUILTIN_TOOLS } from "@ebowwa/ai/tools";
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Types
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
export interface StackConfig {
|
|
36
|
+
/** Enable SSH channel */
|
|
37
|
+
ssh?: {
|
|
38
|
+
chatDir: string;
|
|
39
|
+
pollInterval?: number;
|
|
40
|
+
};
|
|
41
|
+
/** Enable Telegram channel */
|
|
42
|
+
telegram?: {
|
|
43
|
+
botToken?: string;
|
|
44
|
+
allowedChats?: number[];
|
|
45
|
+
};
|
|
46
|
+
/** Enable HTTP API for Ralph management */
|
|
47
|
+
api?: {
|
|
48
|
+
port?: number;
|
|
49
|
+
host?: string;
|
|
50
|
+
};
|
|
51
|
+
/** Ralph loop configuration */
|
|
52
|
+
ralph?: {
|
|
53
|
+
worktreesDir: string;
|
|
54
|
+
repoUrl: string;
|
|
55
|
+
baseBranch?: string;
|
|
56
|
+
};
|
|
57
|
+
/** AI configuration */
|
|
58
|
+
ai?: {
|
|
59
|
+
model?: string;
|
|
60
|
+
temperature?: number;
|
|
61
|
+
maxTokens?: number;
|
|
62
|
+
};
|
|
63
|
+
/** Node identity */
|
|
64
|
+
node?: {
|
|
65
|
+
name: string;
|
|
66
|
+
hostname?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StackState {
|
|
71
|
+
started: Date;
|
|
72
|
+
channels: {
|
|
73
|
+
ssh: boolean;
|
|
74
|
+
telegram: boolean;
|
|
75
|
+
};
|
|
76
|
+
api: {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
port?: number;
|
|
79
|
+
};
|
|
80
|
+
ralphLoops: Map<string, RalphLoopInfo>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface RalphLoopInfo {
|
|
84
|
+
id: string;
|
|
85
|
+
worktree: string;
|
|
86
|
+
prompt: string;
|
|
87
|
+
status: "running" | "paused" | "completed" | "error";
|
|
88
|
+
iterations: number;
|
|
89
|
+
started: Date;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================
|
|
93
|
+
// Stack Class
|
|
94
|
+
// ============================================================
|
|
95
|
+
|
|
96
|
+
export class Stack {
|
|
97
|
+
private config: Required<StackConfig>;
|
|
98
|
+
private state: StackState;
|
|
99
|
+
private router: ReturnType<typeof createChannelRouter>;
|
|
100
|
+
private memory: ReturnType<typeof createPermissionMemory>;
|
|
101
|
+
private client: GLMClient;
|
|
102
|
+
private executor: ToolExecutor;
|
|
103
|
+
private channels: Map<string, ChannelConnector> = new Map();
|
|
104
|
+
private abortController: AbortController | null = null;
|
|
105
|
+
|
|
106
|
+
constructor(config: StackConfig) {
|
|
107
|
+
this.config = {
|
|
108
|
+
ssh: config.ssh ?? { chatDir: "/root/.ssh-chat" },
|
|
109
|
+
telegram: config.telegram ?? {},
|
|
110
|
+
api: config.api ?? { port: 8911 },
|
|
111
|
+
ralph: config.ralph ?? { worktreesDir: "/root/worktrees", repoUrl: "" },
|
|
112
|
+
ai: config.ai ?? { model: "GLM-4.7", temperature: 0.7, maxTokens: 4096 },
|
|
113
|
+
node: config.node ?? { name: "stack", hostname: "localhost" },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
this.state = {
|
|
117
|
+
started: new Date(),
|
|
118
|
+
channels: { ssh: false, telegram: false },
|
|
119
|
+
api: { enabled: !!this.config.api },
|
|
120
|
+
ralphLoops: new Map(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Initialize shared memory
|
|
124
|
+
this.memory = createPermissionMemory({
|
|
125
|
+
channels: {
|
|
126
|
+
ssh: { memoryFile: `${this.config.ssh.chatDir}/memory.json`, maxMessages: 50 },
|
|
127
|
+
telegram: { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 },
|
|
128
|
+
api: { memoryFile: "/root/.api-memory.json", maxMessages: 100 },
|
|
129
|
+
},
|
|
130
|
+
permissions: {
|
|
131
|
+
ssh: { canRead: ["telegram", "api"] },
|
|
132
|
+
telegram: { canRead: ["ssh", "api"] },
|
|
133
|
+
api: { canRead: ["ssh", "telegram"] },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Initialize router
|
|
138
|
+
this.router = createChannelRouter({
|
|
139
|
+
announcement: {
|
|
140
|
+
serverName: this.config.node.name,
|
|
141
|
+
hostname: this.config.node.hostname,
|
|
142
|
+
packageName: "@ebowwa/stack",
|
|
143
|
+
version: "0.1.0",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Initialize AI
|
|
148
|
+
this.client = new GLMClient();
|
|
149
|
+
this.executor = new ToolExecutor(this.client, [...BUILTIN_TOOLS]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================
|
|
153
|
+
// Channel Registration
|
|
154
|
+
// ============================================================
|
|
155
|
+
|
|
156
|
+
async registerSSH(): Promise<void> {
|
|
157
|
+
if (!this.config.ssh) return;
|
|
158
|
+
|
|
159
|
+
console.log("[Stack] Registering SSH channel...");
|
|
160
|
+
const { createSSHChannel } = await import("@ebowwa/channel-ssh");
|
|
161
|
+
const channel = createSSHChannel({
|
|
162
|
+
chatDir: this.config.ssh.chatDir,
|
|
163
|
+
pollInterval: this.config.ssh.pollInterval ?? 500,
|
|
164
|
+
});
|
|
165
|
+
this.channels.set("ssh", channel);
|
|
166
|
+
this.router.register(channel);
|
|
167
|
+
this.state.channels.ssh = true;
|
|
168
|
+
console.log("[Stack] SSH registered");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async registerTelegram(): Promise<void> {
|
|
172
|
+
if (!this.config.telegram?.botToken) {
|
|
173
|
+
console.log("[Stack] Telegram not configured");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log("[Stack] Registering Telegram channel...");
|
|
178
|
+
const { TelegramChannel } = await import("@ebowwa/channel-telegram");
|
|
179
|
+
const channel = new TelegramChannel({
|
|
180
|
+
botToken: this.config.telegram.botToken,
|
|
181
|
+
allowedChats: this.config.telegram.allowedChats,
|
|
182
|
+
});
|
|
183
|
+
this.channels.set("telegram", channel);
|
|
184
|
+
this.router.register(channel);
|
|
185
|
+
this.state.channels.telegram = true;
|
|
186
|
+
console.log("[Stack] Telegram registered");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============================================================
|
|
190
|
+
// Message Handler
|
|
191
|
+
// ============================================================
|
|
192
|
+
|
|
193
|
+
private async handleMessage(routed: { message: ChannelMessage; channelId: ChannelId }): Promise<ChannelResponse> {
|
|
194
|
+
const { message, channelId } = routed;
|
|
195
|
+
const channel = channelId.platform as "ssh" | "telegram" | "api";
|
|
196
|
+
const text = message.text;
|
|
197
|
+
|
|
198
|
+
console.log(`[${channel}] ${text.slice(0, 50)}...`);
|
|
199
|
+
|
|
200
|
+
// Check for memory commands
|
|
201
|
+
const cmdResult = parseMemoryCommand(this.memory, channel, text);
|
|
202
|
+
if (cmdResult.handled) {
|
|
203
|
+
return {
|
|
204
|
+
content: { text: cmdResult.response || "Done" },
|
|
205
|
+
replyTo: { messageId: message.messageId, channelId: message.channelId },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for Ralph commands
|
|
210
|
+
const ralphResult = await this.handleRalphCommand(channel, text);
|
|
211
|
+
if (ralphResult) {
|
|
212
|
+
return {
|
|
213
|
+
content: { text: ralphResult },
|
|
214
|
+
replyTo: { messageId: message.messageId, channelId: message.channelId },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add to shared memory
|
|
219
|
+
this.memory.addMessage(channel, { role: "user", content: text });
|
|
220
|
+
|
|
221
|
+
// Build messages with cross-channel context
|
|
222
|
+
const llmMessages = this.memory.buildLLMMessages(
|
|
223
|
+
channel,
|
|
224
|
+
this.buildSystemPrompt(),
|
|
225
|
+
text,
|
|
226
|
+
{ crossChannelLimit: 10 }
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Execute with tools
|
|
230
|
+
try {
|
|
231
|
+
const result = await this.executor.executeWithTools(llmMessages, {
|
|
232
|
+
systemPrompt: this.buildSystemPrompt(),
|
|
233
|
+
maxIterations: 3,
|
|
234
|
+
temperature: this.config.ai.temperature,
|
|
235
|
+
maxTokens: this.config.ai.maxTokens,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
this.memory.addMessage(channel, { role: "assistant", content: result.content });
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
content: { text: result.content },
|
|
242
|
+
replyTo: { messageId: message.messageId, channelId: message.channelId },
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
246
|
+
return {
|
|
247
|
+
content: { text: `Error: ${errorMsg}` },
|
|
248
|
+
replyTo: { messageId: message.messageId, channelId: message.channelId },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private buildSystemPrompt(): string {
|
|
254
|
+
return `You are **${this.config.node.name}** — a 24/7 AI stack running on this node.
|
|
255
|
+
|
|
256
|
+
## What You Manage
|
|
257
|
+
- **Ralph Loops**: Autonomous AI agents running tasks
|
|
258
|
+
- **Git Worktrees**: Isolated development environments
|
|
259
|
+
- **Node Monitoring**: CPU, memory, disk usage
|
|
260
|
+
- **Cross-Channel Memory**: Shared context between SSH, Telegram, and API
|
|
261
|
+
|
|
262
|
+
## Commands
|
|
263
|
+
- \`/ralph start <prompt>\` — Start a Ralph loop
|
|
264
|
+
- \`/ralph list\` — List running loops
|
|
265
|
+
- \`/ralph stop <id>\` — Stop a loop
|
|
266
|
+
- \`/status\` — Node status
|
|
267
|
+
- \`/memory <cmd>\` — Memory management (grant, revoke, list, clear)
|
|
268
|
+
|
|
269
|
+
## Stack Info
|
|
270
|
+
- Node: ${this.config.node.name}
|
|
271
|
+
- Channels: ${Object.entries(this.state.channels).filter(([, v]) => v).map(([k]) => k).join(", ") || "none"}
|
|
272
|
+
- API: :${this.config.api?.port ?? 8911}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ============================================================
|
|
276
|
+
// Ralph Loop Management (delegates to node-agent)
|
|
277
|
+
// ============================================================
|
|
278
|
+
|
|
279
|
+
private async handleRalphCommand(channel: string, text: string): Promise<string | null> {
|
|
280
|
+
const parts = text.trim().split(/\s+/);
|
|
281
|
+
const cmd = parts[0].toLowerCase();
|
|
282
|
+
|
|
283
|
+
if (cmd === "/ralph" || cmd === "/ralph") {
|
|
284
|
+
const subCmd = parts[1]?.toLowerCase();
|
|
285
|
+
|
|
286
|
+
switch (subCmd) {
|
|
287
|
+
case "start": {
|
|
288
|
+
const prompt = parts.slice(2).join(" ");
|
|
289
|
+
if (!prompt) return "Usage: /ralph start <prompt>";
|
|
290
|
+
return await this.startRalphLoop(prompt);
|
|
291
|
+
}
|
|
292
|
+
case "list":
|
|
293
|
+
return this.listRalphLoops();
|
|
294
|
+
case "stop": {
|
|
295
|
+
const id = parts[2];
|
|
296
|
+
if (!id) return "Usage: /ralph stop <id>";
|
|
297
|
+
return await this.stopRalphLoop(id);
|
|
298
|
+
}
|
|
299
|
+
default:
|
|
300
|
+
return "Ralph commands: start, list, stop";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (cmd === "/status") {
|
|
305
|
+
return this.getStatus();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async startRalphLoop(prompt: string): Promise<string> {
|
|
312
|
+
// TODO: Delegate to @ebowwa/node-agent RalphService
|
|
313
|
+
const id = `ralph-${Date.now()}`;
|
|
314
|
+
this.state.ralphLoops.set(id, {
|
|
315
|
+
id,
|
|
316
|
+
worktree: `${this.config.ralph.worktreesDir}/${id}`,
|
|
317
|
+
prompt,
|
|
318
|
+
status: "running",
|
|
319
|
+
iterations: 0,
|
|
320
|
+
started: new Date(),
|
|
321
|
+
});
|
|
322
|
+
return `Started Ralph loop: ${id}\nPrompt: ${prompt.slice(0, 100)}...`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private listRalphLoops(): string {
|
|
326
|
+
if (this.state.ralphLoops.size === 0) {
|
|
327
|
+
return "No Ralph loops running";
|
|
328
|
+
}
|
|
329
|
+
const lines = ["Ralph Loops:"];
|
|
330
|
+
for (const [id, info] of this.state.ralphLoops) {
|
|
331
|
+
lines.push(` ${id}: ${info.status} (${info.iterations} iters)`);
|
|
332
|
+
}
|
|
333
|
+
return lines.join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async stopRalphLoop(id: string): Promise<string> {
|
|
337
|
+
const info = this.state.ralphLoops.get(id);
|
|
338
|
+
if (!info) return `Ralph loop not found: ${id}`;
|
|
339
|
+
info.status = "paused";
|
|
340
|
+
return `Stopped Ralph loop: ${id}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private getStatus(): string {
|
|
344
|
+
const lines = [
|
|
345
|
+
`**${this.config.node.name} Status**`,
|
|
346
|
+
`Channels: SSH=${this.state.channels.ssh}, Telegram=${this.state.channels.telegram}`,
|
|
347
|
+
`Ralph Loops: ${this.state.ralphLoops.size}`,
|
|
348
|
+
`Uptime: ${Math.floor((Date.now() - this.state.started.getTime()) / 1000)}s`,
|
|
349
|
+
];
|
|
350
|
+
return lines.join("\n");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ============================================================
|
|
354
|
+
// HTTP API (optional, delegates to node-agent)
|
|
355
|
+
// ============================================================
|
|
356
|
+
|
|
357
|
+
private startAPI(): void {
|
|
358
|
+
if (!this.config.api) return;
|
|
359
|
+
|
|
360
|
+
const port = this.config.api.port ?? 8911;
|
|
361
|
+
console.log(`[Stack] API would start on :${port} (implement with Bun.serve)`);
|
|
362
|
+
|
|
363
|
+
// TODO: Start HTTP server with routes:
|
|
364
|
+
// GET /api/status - Stack status
|
|
365
|
+
// GET /api/ralph-loops - List Ralph loops
|
|
366
|
+
// POST /api/ralph-loops - Start Ralph loop
|
|
367
|
+
// DELETE /api/ralph-loops/:id - Stop Ralph loop
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================
|
|
371
|
+
// Lifecycle
|
|
372
|
+
// ============================================================
|
|
373
|
+
|
|
374
|
+
async start(): Promise<void> {
|
|
375
|
+
console.log(`[Stack] Starting ${this.config.node.name}...`);
|
|
376
|
+
|
|
377
|
+
this.abortController = new AbortController();
|
|
378
|
+
|
|
379
|
+
// Register channels
|
|
380
|
+
await this.registerSSH();
|
|
381
|
+
await this.registerTelegram();
|
|
382
|
+
|
|
383
|
+
// Set handler
|
|
384
|
+
this.router.setHandler((routed) => this.handleMessage(routed));
|
|
385
|
+
|
|
386
|
+
// Start router
|
|
387
|
+
await this.router.start();
|
|
388
|
+
|
|
389
|
+
// Start API (optional)
|
|
390
|
+
this.startAPI();
|
|
391
|
+
|
|
392
|
+
console.log("[Stack] Running!");
|
|
393
|
+
console.log(` - SSH: echo 'msg' > ${this.config.ssh.chatDir}/in`);
|
|
394
|
+
if (this.state.channels.telegram) {
|
|
395
|
+
console.log(" - Telegram: enabled");
|
|
396
|
+
}
|
|
397
|
+
console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
|
|
398
|
+
|
|
399
|
+
// Keep running
|
|
400
|
+
await new Promise(() => {}); // Run forever
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async stop(): Promise<void> {
|
|
404
|
+
console.log("[Stack] Stopping...");
|
|
405
|
+
this.abortController?.abort();
|
|
406
|
+
await this.router.stop();
|
|
407
|
+
|
|
408
|
+
// Stop all channels
|
|
409
|
+
for (const [name, channel] of this.channels) {
|
|
410
|
+
console.log(`[Stack] Stopping ${name}...`);
|
|
411
|
+
await channel.stop();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log("[Stack] Stopped");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================
|
|
419
|
+
// CLI Entry Point
|
|
420
|
+
// ============================================================
|
|
421
|
+
|
|
422
|
+
async function main() {
|
|
423
|
+
const stack = new Stack({
|
|
424
|
+
ssh: {
|
|
425
|
+
chatDir: process.env.SSH_CHAT_DIR || "/root/.ssh-chat",
|
|
426
|
+
pollInterval: 500,
|
|
427
|
+
},
|
|
428
|
+
telegram: {
|
|
429
|
+
botToken: process.env.TELEGRAM_BOT_TOKEN,
|
|
430
|
+
allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined,
|
|
431
|
+
},
|
|
432
|
+
api: {
|
|
433
|
+
port: parseInt(process.env.API_PORT || "8911", 10),
|
|
434
|
+
},
|
|
435
|
+
ralph: {
|
|
436
|
+
worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
|
|
437
|
+
repoUrl: process.env.REPO_URL || "",
|
|
438
|
+
},
|
|
439
|
+
node: {
|
|
440
|
+
name: process.env.NODE_NAME || "stack",
|
|
441
|
+
hostname: process.env.HOSTNAME || "localhost",
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Handle shutdown
|
|
446
|
+
process.on("SIGINT", async () => {
|
|
447
|
+
await stack.stop();
|
|
448
|
+
process.exit(0);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
process.on("SIGTERM", async () => {
|
|
452
|
+
await stack.stop();
|
|
453
|
+
process.exit(0);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await stack.start();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Run if executed directly
|
|
460
|
+
if (import.meta.main) {
|
|
461
|
+
main().catch((error) => {
|
|
462
|
+
console.error("[Stack] Fatal error:", error);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export default Stack;
|