@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/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;