@ebowwa/stack 0.1.4 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +26 -11
  2. package/dist/index.js +25 -136
  3. package/package.json +7 -10
  4. package/src/index.ts +49 -199
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @ebowwa/stack
2
2
 
3
- Full-stack daemon orchestrator combining unified-router (cross-channel communication) and node-agent (Ralph orchestration).
3
+ Cross-channel AI stack with shared memory.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Unified Router**: Cross-channel communication (SSH + Telegram)
8
- - **Node Agent**: Ralph loop orchestration, worktrees, monitoring
9
- - **Shared Memory**: Cross-channel context with permission controls
10
- - **HTTP API**: REST endpoints for management (port 8911)
7
+ - **Channels**: SSH, Telegram (both optional)
8
+ - **Cross-Channel Memory**: Shared context between channels with permission controls
9
+ - **AI Brain**: GLM-powered message handling with tool execution
10
+ - **Minimal API**: Status and health endpoints
11
11
 
12
12
  ## Installation
13
13
 
@@ -18,18 +18,33 @@ bun add @ebowwa/stack
18
18
  ## Usage
19
19
 
20
20
  ```bash
21
- # Start the stack
22
- bun run @ebowwa/stack
21
+ # Telegram only
22
+ TELEGRAM_BOT_TOKEN=xxx bun run @ebowwa/stack
23
23
 
24
- # Or via environment
24
+ # SSH only
25
+ SSH_CHAT_DIR=/root/.ssh-chat bun run @ebowwa/stack
26
+
27
+ # Both channels
25
28
  SSH_CHAT_DIR=/root/.ssh-chat TELEGRAM_BOT_TOKEN=xxx bun run @ebowwa/stack
26
29
  ```
27
30
 
31
+ ## Environment Variables
32
+
33
+ | Variable | Description |
34
+ |----------|-------------|
35
+ | `SSH_CHAT_DIR` | SSH chat directory (enables SSH channel) |
36
+ | `TELEGRAM_BOT_TOKEN` | Telegram bot token (enables Telegram) |
37
+ | `TELEGRAM_CHAT_ID` | Allowed Telegram chat ID |
38
+ | `API_PORT` | API port (default: 8911) |
39
+ | `NODE_NAME` | Node name (default: stack) |
40
+
41
+ ## API Endpoints
42
+
43
+ - `GET /api/status` - Stack status
44
+ - `GET /health` - Health check
45
+
28
46
  ## Commands
29
47
 
30
- - `/ralph start <prompt>` - Start a Ralph loop
31
- - `/ralph list` - List running loops
32
- - `/ralph stop <id>` - Stop a loop
33
48
  - `/status` - Node status
34
49
  - `/memory <cmd>` - Memory management (grant, revoke, list, clear)
35
50
 
package/dist/index.js CHANGED
@@ -57410,15 +57410,13 @@ class Stack {
57410
57410
  this.config = {
57411
57411
  ...config,
57412
57412
  api: config.api ?? { port: 8911 },
57413
- ralph: config.ralph ?? { worktreesDir: "/root/worktrees", repoUrl: "" },
57414
57413
  ai: config.ai ?? { model: "GLM-4.7", temperature: 0.7, maxTokens: 4096 },
57415
57414
  node: config.node ?? { name: "stack", hostname: "localhost" }
57416
57415
  };
57417
57416
  this.state = {
57418
57417
  started: new Date,
57419
57418
  channels: { ssh: false, telegram: false },
57420
- api: { enabled: !!this.config.api, port: this.config.api?.port },
57421
- ralphLoops: new Map
57419
+ api: { enabled: !!this.config.api, port: this.config.api?.port }
57422
57420
  };
57423
57421
  const memoryChannels = {};
57424
57422
  const permissions = {};
@@ -57431,10 +57429,6 @@ class Stack {
57431
57429
  memoryChannels.telegram = { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 };
57432
57430
  enabledChannels.push("telegram");
57433
57431
  }
57434
- if (this.config.api) {
57435
- memoryChannels.api = { memoryFile: "/root/.api-memory.json", maxMessages: 100 };
57436
- enabledChannels.push("api");
57437
- }
57438
57432
  for (const channel of enabledChannels) {
57439
57433
  permissions[channel] = { canRead: enabledChannels.filter((c) => c !== channel) };
57440
57434
  }
@@ -57447,7 +57441,7 @@ class Stack {
57447
57441
  serverName: this.config.node.name,
57448
57442
  hostname: this.config.node.hostname,
57449
57443
  packageName: "@ebowwa/stack",
57450
- version: "0.1.1"
57444
+ version: "0.2.0"
57451
57445
  }
57452
57446
  });
57453
57447
  this.client = new GLMClient;
@@ -57506,11 +57500,10 @@ class Stack {
57506
57500
  replyTo: { messageId: message.messageId, channelId: message.channelId }
57507
57501
  };
57508
57502
  }
57509
- const ralphResult = await this.handleRalphCommand(channel, text);
57510
- if (ralphResult) {
57503
+ if (text.trim().toLowerCase() === "/status") {
57511
57504
  stopTyping();
57512
57505
  return {
57513
- content: { text: ralphResult },
57506
+ content: { text: this.getStatus() },
57514
57507
  replyTo: { messageId: message.messageId, channelId: message.channelId }
57515
57508
  };
57516
57509
  }
@@ -57539,91 +57532,27 @@ class Stack {
57539
57532
  }
57540
57533
  }
57541
57534
  buildSystemPrompt() {
57542
- return `You are **${this.config.node.name}** \u2014 a 24/7 AI stack running on this node.
57535
+ const channelList = Object.entries(this.state.channels).filter(([, v]) => v).map(([k]) => k).join(", ") || "none";
57536
+ return `You are **${this.config.node.name}** \u2014 a 24/7 AI assistant.
57543
57537
 
57544
- ## What You Manage
57545
- - **Ralph Loops**: Autonomous AI agents running tasks
57546
- - **Git Worktrees**: Isolated development environments
57547
- - **Node Monitoring**: CPU, memory, disk usage
57548
- - **Cross-Channel Memory**: Shared context between SSH, Telegram, and API
57538
+ ## Capabilities
57539
+ - Cross-channel memory (shared context between channels)
57540
+ - Tool execution via MCP
57541
+ - Node monitoring and management
57549
57542
 
57550
57543
  ## Commands
57551
- - \`/ralph start <prompt>\` \u2014 Start a Ralph loop
57552
- - \`/ralph list\` \u2014 List running loops
57553
- - \`/ralph stop <id>\` \u2014 Stop a loop
57554
57544
  - \`/status\` \u2014 Node status
57555
57545
  - \`/memory <cmd>\` \u2014 Memory management (grant, revoke, list, clear)
57556
57546
 
57557
- ## Stack Info
57547
+ ## Info
57558
57548
  - Node: ${this.config.node.name}
57559
- - Channels: ${Object.entries(this.state.channels).filter(([, v]) => v).map(([k]) => k).join(", ") || "none"}
57549
+ - Channels: ${channelList}
57560
57550
  - API: :${this.config.api?.port ?? 8911}`;
57561
57551
  }
57562
- async handleRalphCommand(channel, text) {
57563
- const parts = text.trim().split(/\s+/);
57564
- const cmd = parts[0].toLowerCase();
57565
- if (cmd === "/ralph" || cmd === "/ralph") {
57566
- const subCmd = parts[1]?.toLowerCase();
57567
- switch (subCmd) {
57568
- case "start": {
57569
- const prompt = parts.slice(2).join(" ");
57570
- if (!prompt)
57571
- return "Usage: /ralph start <prompt>";
57572
- return await this.startRalphLoop(prompt);
57573
- }
57574
- case "list":
57575
- return this.listRalphLoops();
57576
- case "stop": {
57577
- const id = parts[2];
57578
- if (!id)
57579
- return "Usage: /ralph stop <id>";
57580
- return await this.stopRalphLoop(id);
57581
- }
57582
- default:
57583
- return "Ralph commands: start, list, stop";
57584
- }
57585
- }
57586
- if (cmd === "/status") {
57587
- return this.getStatus();
57588
- }
57589
- return null;
57590
- }
57591
- async startRalphLoop(prompt) {
57592
- const id = `ralph-${Date.now()}`;
57593
- this.state.ralphLoops.set(id, {
57594
- id,
57595
- worktree: `${this.config.ralph.worktreesDir}/${id}`,
57596
- prompt,
57597
- status: "running",
57598
- iterations: 0,
57599
- started: new Date
57600
- });
57601
- return `Started Ralph loop: ${id}
57602
- Prompt: ${prompt.slice(0, 100)}...`;
57603
- }
57604
- listRalphLoops() {
57605
- if (this.state.ralphLoops.size === 0) {
57606
- return "No Ralph loops running";
57607
- }
57608
- const lines = ["Ralph Loops:"];
57609
- for (const [id, info] of this.state.ralphLoops) {
57610
- lines.push(` ${id}: ${info.status} (${info.iterations} iters)`);
57611
- }
57612
- return lines.join(`
57613
- `);
57614
- }
57615
- async stopRalphLoop(id) {
57616
- const info = this.state.ralphLoops.get(id);
57617
- if (!info)
57618
- return `Ralph loop not found: ${id}`;
57619
- info.status = "paused";
57620
- return `Stopped Ralph loop: ${id}`;
57621
- }
57622
57552
  getStatus() {
57623
57553
  const lines = [
57624
57554
  `**${this.config.node.name} Status**`,
57625
57555
  `Channels: SSH=${this.state.channels.ssh}, Telegram=${this.state.channels.telegram}`,
57626
- `Ralph Loops: ${this.state.ralphLoops.size}`,
57627
57556
  `Uptime: ${Math.floor((Date.now() - this.state.started.getTime()) / 1000)}s`
57628
57557
  ];
57629
57558
  return lines.join(`
@@ -57634,7 +57563,7 @@ Prompt: ${prompt.slice(0, 100)}...`;
57634
57563
  return;
57635
57564
  const port = this.config.api.port ?? 8911;
57636
57565
  const host = this.config.api.host ?? "0.0.0.0";
57637
- const server = Bun.serve({
57566
+ Bun.serve({
57638
57567
  port,
57639
57568
  host,
57640
57569
  fetch: async (req) => {
@@ -57642,63 +57571,27 @@ Prompt: ${prompt.slice(0, 100)}...`;
57642
57571
  const path = url.pathname;
57643
57572
  const corsHeaders = {
57644
57573
  "Access-Control-Allow-Origin": "*",
57645
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
57574
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
57646
57575
  "Access-Control-Allow-Headers": "Content-Type"
57647
57576
  };
57648
57577
  if (req.method === "OPTIONS") {
57649
57578
  return new Response(null, { headers: corsHeaders });
57650
57579
  }
57651
- try {
57652
- if (path === "/api/status" && req.method === "GET") {
57653
- return Response.json(this.getStatusJSON(), { headers: corsHeaders });
57654
- }
57655
- if (path === "/api/ralph-loops" && req.method === "GET") {
57656
- return Response.json(this.listRalphLoopsJSON(), { headers: corsHeaders });
57657
- }
57658
- if (path === "/api/ralph-loops" && req.method === "POST") {
57659
- const body = await req.json();
57660
- const prompt = body.prompt;
57661
- if (!prompt) {
57662
- return Response.json({ error: "Missing prompt" }, { status: 400, headers: corsHeaders });
57663
- }
57664
- const result = await this.startRalphLoop(prompt);
57665
- return Response.json({ message: result }, { headers: corsHeaders });
57666
- }
57667
- const match = path.match(/^\/api\/ralph-loops\/(.+)$/);
57668
- if (match && req.method === "DELETE") {
57669
- const result = await this.stopRalphLoop(match[1]);
57670
- return Response.json({ message: result }, { headers: corsHeaders });
57671
- }
57672
- if (path === "/health") {
57673
- return Response.json({ status: "ok" }, { headers: corsHeaders });
57674
- }
57675
- return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
57676
- } catch (error) {
57677
- const errorMsg = error instanceof Error ? error.message : String(error);
57678
- return Response.json({ error: errorMsg }, { status: 500, headers: corsHeaders });
57580
+ if (path === "/api/status" && req.method === "GET") {
57581
+ return Response.json({
57582
+ node: this.config.node.name,
57583
+ channels: this.state.channels,
57584
+ uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000)
57585
+ }, { headers: corsHeaders });
57679
57586
  }
57587
+ if (path === "/health") {
57588
+ return Response.json({ status: "ok" }, { headers: corsHeaders });
57589
+ }
57590
+ return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
57680
57591
  }
57681
57592
  });
57682
57593
  console.log(`[Stack] API running on http://${host}:${port}`);
57683
57594
  }
57684
- getStatusJSON() {
57685
- return {
57686
- node: this.config.node.name,
57687
- channels: {
57688
- ssh: this.state.channels.ssh,
57689
- telegram: this.state.channels.telegram
57690
- },
57691
- api: {
57692
- enabled: this.state.api.enabled,
57693
- port: this.state.api.port
57694
- },
57695
- ralphLoops: this.state.ralphLoops.size,
57696
- uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000)
57697
- };
57698
- }
57699
- listRalphLoopsJSON() {
57700
- return Array.from(this.state.ralphLoops.values());
57701
- }
57702
57595
  async start() {
57703
57596
  console.log(`[Stack] Starting ${this.config.node.name}...`);
57704
57597
  this.abortController = new AbortController;
@@ -57717,7 +57610,7 @@ Prompt: ${prompt.slice(0, 100)}...`;
57717
57610
  if (this.state.api.enabled) {
57718
57611
  console.log(` - API: :${this.state.api.port}`);
57719
57612
  }
57720
- console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
57613
+ console.log(" - Commands: /status, /memory <cmd>");
57721
57614
  await new Promise(() => {});
57722
57615
  }
57723
57616
  async stop() {
@@ -57749,10 +57642,6 @@ async function main() {
57749
57642
  port: parseInt(process.env.API_PORT || "8911", 10),
57750
57643
  host: process.env.API_HOST
57751
57644
  },
57752
- ralph: {
57753
- worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
57754
- repoUrl: process.env.REPO_URL || ""
57755
- },
57756
57645
  node: {
57757
57646
  name: process.env.NODE_NAME || "stack",
57758
57647
  hostname: process.env.HOSTNAME || "localhost"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ebowwa/stack",
3
- "version": "0.1.4",
4
- "description": "Full-stack daemon orchestrator combining unified-router (cross-channel) and node-agent (Ralph orchestration)",
3
+ "version": "0.2.0",
4
+ "description": "Cross-channel AI stack with shared memory (SSH + Telegram)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -20,12 +20,12 @@
20
20
  },
21
21
  "keywords": [
22
22
  "stack",
23
- "orchestrator",
24
- "daemon",
25
- "ralph",
23
+ "channel",
26
24
  "telegram",
27
25
  "ssh",
28
- "ai"
26
+ "ai",
27
+ "cross-channel",
28
+ "memory"
29
29
  ],
30
30
  "author": "Ebowwa Labs <labs@ebowwa.com>",
31
31
  "license": "MIT",
@@ -40,10 +40,7 @@
40
40
  "@ebowwa/channel-ssh": "^2.1.1",
41
41
  "@ebowwa/channel-telegram": "^1.14.2",
42
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"
43
+ "@ebowwa/codespaces-types": "^1.6.1"
47
44
  },
48
45
  "devDependencies": {
49
46
  "@types/bun": "latest"
package/src/index.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * @ebowwa/stack - Full-Stack Daemon Orchestrator
3
+ * @ebowwa/stack - Cross-Channel AI Stack
4
4
  *
5
- * Combines:
5
+ * Features:
6
6
  * - Unified Router: Cross-channel communication (SSH + Telegram)
7
- * - Node Agent: Ralph loop orchestration, worktrees, monitoring
7
+ * - Cross-channel memory with permission controls
8
+ * - AI-powered message handling
8
9
  *
9
10
  * Architecture:
10
11
  * ┌──────────────────────────────────────────────────────────────┐
11
12
  * │ STACK │
12
13
  * │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
13
- * │ │ Unified Router │ │ Node Agent │ │ HTTP API │ │
14
- * │ │ (chat/messaging)│ │ (Ralph loops) │ (:8911) │ │
14
+ * │ │ SSH Channel │ │Telegram Channel│ │ HTTP API │ │
15
+ * │ │ (optional) │ │ (optional) │ (optional) │ │
15
16
  * │ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │
16
17
  * │ │ │ │ │
17
18
  * │ └───────────────────┴───────────────────┘ │
@@ -19,6 +20,10 @@
19
20
  * │ ┌─────────▼─────────┐ │
20
21
  * │ │ Shared Memory │ │
21
22
  * │ │ (Cross-Context) │ │
23
+ * │ └─────────┬─────────┘ │
24
+ * │ │ │
25
+ * │ ┌─────────▼─────────┐ │
26
+ * │ │ AI Brain (GLM) │ │
22
27
  * │ └───────────────────┘ │
23
28
  * └──────────────────────────────────────────────────────────────┘
24
29
  */
@@ -43,17 +48,11 @@ export interface StackConfig {
43
48
  botToken?: string;
44
49
  allowedChats?: number[];
45
50
  };
46
- /** Enable HTTP API for Ralph management */
51
+ /** Enable HTTP API */
47
52
  api?: {
48
53
  port?: number;
49
54
  host?: string;
50
55
  };
51
- /** Ralph loop configuration */
52
- ralph?: {
53
- worktreesDir: string;
54
- repoUrl: string;
55
- baseBranch?: string;
56
- };
57
56
  /** AI configuration */
58
57
  ai?: {
59
58
  model?: string;
@@ -77,16 +76,6 @@ export interface StackState {
77
76
  enabled: boolean;
78
77
  port?: number;
79
78
  };
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
79
  }
91
80
 
92
81
  // ============================================================
@@ -104,21 +93,17 @@ export class Stack {
104
93
  private abortController: AbortController | null = null;
105
94
 
106
95
  constructor(config: StackConfig) {
107
- // Only set defaults for non-channel config
108
96
  this.config = {
109
97
  ...config,
110
98
  api: config.api ?? { port: 8911 },
111
- ralph: config.ralph ?? { worktreesDir: "/root/worktrees", repoUrl: "" },
112
99
  ai: config.ai ?? { model: "GLM-4.7", temperature: 0.7, maxTokens: 4096 },
113
100
  node: config.node ?? { name: "stack", hostname: "localhost" },
114
- // ssh and telegram remain undefined if not provided
115
101
  };
116
102
 
117
103
  this.state = {
118
104
  started: new Date(),
119
105
  channels: { ssh: false, telegram: false },
120
106
  api: { enabled: !!this.config.api, port: this.config.api?.port },
121
- ralphLoops: new Map(),
122
107
  };
123
108
 
124
109
  // Build memory channels dynamically based on enabled channels
@@ -134,17 +119,13 @@ export class Stack {
134
119
  memoryChannels.telegram = { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 };
135
120
  enabledChannels.push("telegram");
136
121
  }
137
- if (this.config.api) {
138
- memoryChannels.api = { memoryFile: "/root/.api-memory.json", maxMessages: 100 };
139
- enabledChannels.push("api");
140
- }
141
122
 
142
- // Set up cross-channel permissions for enabled channels
123
+ // Set up cross-channel permissions
143
124
  for (const channel of enabledChannels) {
144
125
  permissions[channel] = { canRead: enabledChannels.filter(c => c !== channel) };
145
126
  }
146
127
 
147
- // Initialize shared memory (empty if no channels)
128
+ // Initialize shared memory
148
129
  this.memory = createPermissionMemory({
149
130
  channels: memoryChannels,
150
131
  permissions,
@@ -156,7 +137,7 @@ export class Stack {
156
137
  serverName: this.config.node.name,
157
138
  hostname: this.config.node.hostname,
158
139
  packageName: "@ebowwa/stack",
159
- version: "0.1.1",
140
+ version: "0.2.0",
160
141
  },
161
142
  });
162
143
 
@@ -208,7 +189,7 @@ export class Stack {
208
189
 
209
190
  private async handleMessage(routed: { message: ChannelMessage; channelId: ChannelId }): Promise<ChannelResponse> {
210
191
  const { message, channelId } = routed;
211
- const channel = channelId.platform as "ssh" | "telegram" | "api";
192
+ const channel = channelId.platform as "ssh" | "telegram";
212
193
  const text = message.text;
213
194
 
214
195
  console.log(`[${channel}] ${text.slice(0, 50)}...`);
@@ -236,12 +217,11 @@ export class Stack {
236
217
  };
237
218
  }
238
219
 
239
- // Check for Ralph commands
240
- const ralphResult = await this.handleRalphCommand(channel, text);
241
- if (ralphResult) {
220
+ // Check for status command
221
+ if (text.trim().toLowerCase() === "/status") {
242
222
  stopTyping();
243
223
  return {
244
- content: { text: ralphResult },
224
+ content: { text: this.getStatus() },
245
225
  replyTo: { messageId: message.messageId, channelId: message.channelId },
246
226
  };
247
227
  }
@@ -284,107 +264,39 @@ export class Stack {
284
264
  }
285
265
 
286
266
  private buildSystemPrompt(): string {
287
- return `You are **${this.config.node.name}** — a 24/7 AI stack running on this node.
267
+ const channelList = Object.entries(this.state.channels)
268
+ .filter(([, v]) => v)
269
+ .map(([k]) => k)
270
+ .join(", ") || "none";
271
+
272
+ return `You are **${this.config.node.name}** — a 24/7 AI assistant.
288
273
 
289
- ## What You Manage
290
- - **Ralph Loops**: Autonomous AI agents running tasks
291
- - **Git Worktrees**: Isolated development environments
292
- - **Node Monitoring**: CPU, memory, disk usage
293
- - **Cross-Channel Memory**: Shared context between SSH, Telegram, and API
274
+ ## Capabilities
275
+ - Cross-channel memory (shared context between channels)
276
+ - Tool execution via MCP
277
+ - Node monitoring and management
294
278
 
295
279
  ## Commands
296
- - \`/ralph start <prompt>\` — Start a Ralph loop
297
- - \`/ralph list\` — List running loops
298
- - \`/ralph stop <id>\` — Stop a loop
299
280
  - \`/status\` — Node status
300
281
  - \`/memory <cmd>\` — Memory management (grant, revoke, list, clear)
301
282
 
302
- ## Stack Info
283
+ ## Info
303
284
  - Node: ${this.config.node.name}
304
- - Channels: ${Object.entries(this.state.channels).filter(([, v]) => v).map(([k]) => k).join(", ") || "none"}
285
+ - Channels: ${channelList}
305
286
  - API: :${this.config.api?.port ?? 8911}`;
306
287
  }
307
288
 
308
- // ============================================================
309
- // Ralph Loop Management (delegates to node-agent)
310
- // ============================================================
311
-
312
- private async handleRalphCommand(channel: string, text: string): Promise<string | null> {
313
- const parts = text.trim().split(/\s+/);
314
- const cmd = parts[0].toLowerCase();
315
-
316
- if (cmd === "/ralph" || cmd === "/ralph") {
317
- const subCmd = parts[1]?.toLowerCase();
318
-
319
- switch (subCmd) {
320
- case "start": {
321
- const prompt = parts.slice(2).join(" ");
322
- if (!prompt) return "Usage: /ralph start <prompt>";
323
- return await this.startRalphLoop(prompt);
324
- }
325
- case "list":
326
- return this.listRalphLoops();
327
- case "stop": {
328
- const id = parts[2];
329
- if (!id) return "Usage: /ralph stop <id>";
330
- return await this.stopRalphLoop(id);
331
- }
332
- default:
333
- return "Ralph commands: start, list, stop";
334
- }
335
- }
336
-
337
- if (cmd === "/status") {
338
- return this.getStatus();
339
- }
340
-
341
- return null;
342
- }
343
-
344
- private async startRalphLoop(prompt: string): Promise<string> {
345
- // TODO: Delegate to @ebowwa/node-agent RalphService
346
- const id = `ralph-${Date.now()}`;
347
- this.state.ralphLoops.set(id, {
348
- id,
349
- worktree: `${this.config.ralph.worktreesDir}/${id}`,
350
- prompt,
351
- status: "running",
352
- iterations: 0,
353
- started: new Date(),
354
- });
355
- return `Started Ralph loop: ${id}\nPrompt: ${prompt.slice(0, 100)}...`;
356
- }
357
-
358
- private listRalphLoops(): string {
359
- if (this.state.ralphLoops.size === 0) {
360
- return "No Ralph loops running";
361
- }
362
- const lines = ["Ralph Loops:"];
363
- for (const [id, info] of this.state.ralphLoops) {
364
- lines.push(` ${id}: ${info.status} (${info.iterations} iters)`);
365
- }
366
- return lines.join("\n");
367
- }
368
-
369
- private async stopRalphLoop(id: string): Promise<string> {
370
- const info = this.state.ralphLoops.get(id);
371
- if (!info) return `Ralph loop not found: ${id}`;
372
- info.status = "paused";
373
- return `Stopped Ralph loop: ${id}`;
374
- }
375
-
376
289
  private getStatus(): string {
377
290
  const lines = [
378
291
  `**${this.config.node.name} Status**`,
379
292
  `Channels: SSH=${this.state.channels.ssh}, Telegram=${this.state.channels.telegram}`,
380
- `Ralph Loops: ${this.state.ralphLoops.size}`,
381
293
  `Uptime: ${Math.floor((Date.now() - this.state.started.getTime()) / 1000)}s`,
382
294
  ];
383
295
  return lines.join("\n");
384
296
  }
385
297
 
386
298
  // ============================================================
387
- // HTTP API
299
+ // HTTP API (minimal)
388
300
  // ============================================================
389
301
 
390
302
  private startAPI(): void {
@@ -393,17 +305,16 @@ export class Stack {
393
305
  const port = this.config.api.port ?? 8911;
394
306
  const host = this.config.api.host ?? "0.0.0.0";
395
307
 
396
- const server = Bun.serve({
308
+ Bun.serve({
397
309
  port,
398
310
  host,
399
311
  fetch: async (req) => {
400
312
  const url = new URL(req.url);
401
313
  const path = url.pathname;
402
314
 
403
- // CORS headers
404
315
  const corsHeaders = {
405
316
  "Access-Control-Allow-Origin": "*",
406
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
317
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
407
318
  "Access-Control-Allow-Headers": "Content-Type",
408
319
  };
409
320
 
@@ -411,69 +322,25 @@ export class Stack {
411
322
  return new Response(null, { headers: corsHeaders });
412
323
  }
413
324
 
414
- try {
415
- // GET /api/status
416
- if (path === "/api/status" && req.method === "GET") {
417
- return Response.json(this.getStatusJSON(), { headers: corsHeaders });
418
- }
419
-
420
- // GET /api/ralph-loops
421
- if (path === "/api/ralph-loops" && req.method === "GET") {
422
- return Response.json(this.listRalphLoopsJSON(), { headers: corsHeaders });
423
- }
424
-
425
- // POST /api/ralph-loops
426
- if (path === "/api/ralph-loops" && req.method === "POST") {
427
- const body = await req.json();
428
- const prompt = body.prompt;
429
- if (!prompt) {
430
- return Response.json({ error: "Missing prompt" }, { status: 400, headers: corsHeaders });
431
- }
432
- const result = await this.startRalphLoop(prompt);
433
- return Response.json({ message: result }, { headers: corsHeaders });
434
- }
435
-
436
- // DELETE /api/ralph-loops/:id
437
- const match = path.match(/^\/api\/ralph-loops\/(.+)$/);
438
- if (match && req.method === "DELETE") {
439
- const result = await this.stopRalphLoop(match[1]);
440
- return Response.json({ message: result }, { headers: corsHeaders });
441
- }
442
-
443
- // GET /health
444
- if (path === "/health") {
445
- return Response.json({ status: "ok" }, { headers: corsHeaders });
446
- }
447
-
448
- return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
449
- } catch (error) {
450
- const errorMsg = error instanceof Error ? error.message : String(error);
451
- return Response.json({ error: errorMsg }, { status: 500, headers: corsHeaders });
325
+ // GET /api/status
326
+ if (path === "/api/status" && req.method === "GET") {
327
+ return Response.json({
328
+ node: this.config.node.name,
329
+ channels: this.state.channels,
330
+ uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000),
331
+ }, { headers: corsHeaders });
452
332
  }
453
- },
454
- });
455
333
 
456
- console.log(`[Stack] API running on http://${host}:${port}`);
457
- }
334
+ // GET /health
335
+ if (path === "/health") {
336
+ return Response.json({ status: "ok" }, { headers: corsHeaders });
337
+ }
458
338
 
459
- private getStatusJSON(): object {
460
- return {
461
- node: this.config.node.name,
462
- channels: {
463
- ssh: this.state.channels.ssh,
464
- telegram: this.state.channels.telegram,
465
- },
466
- api: {
467
- enabled: this.state.api.enabled,
468
- port: this.state.api.port,
339
+ return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
469
340
  },
470
- ralphLoops: this.state.ralphLoops.size,
471
- uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000),
472
- };
473
- }
341
+ });
474
342
 
475
- private listRalphLoopsJSON(): object[] {
476
- return Array.from(this.state.ralphLoops.values());
343
+ console.log(`[Stack] API running on http://${host}:${port}`);
477
344
  }
478
345
 
479
346
  // ============================================================
@@ -485,17 +352,12 @@ export class Stack {
485
352
 
486
353
  this.abortController = new AbortController();
487
354
 
488
- // Register channels (only if configured)
489
355
  await this.registerSSH();
490
356
  await this.registerTelegram();
491
357
 
492
- // Set handler
493
358
  this.router.setHandler((routed) => this.handleMessage(routed));
494
-
495
- // Start router
496
359
  await this.router.start();
497
360
 
498
- // Start API (optional)
499
361
  this.startAPI();
500
362
 
501
363
  console.log("[Stack] Running!");
@@ -508,10 +370,9 @@ export class Stack {
508
370
  if (this.state.api.enabled) {
509
371
  console.log(` - API: :${this.state.api.port}`);
510
372
  }
511
- console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
373
+ console.log(" - Commands: /status, /memory <cmd>");
512
374
 
513
- // Keep running
514
- await new Promise(() => {}); // Run forever
375
+ await new Promise(() => {});
515
376
  }
516
377
 
517
378
  async stop(): Promise<void> {
@@ -519,7 +380,6 @@ export class Stack {
519
380
  this.abortController?.abort();
520
381
  await this.router.stop();
521
382
 
522
- // Stop all channels
523
383
  for (const [name, channel] of this.channels) {
524
384
  console.log(`[Stack] Stopping ${name}...`);
525
385
  await channel.stop();
@@ -534,31 +394,23 @@ export class Stack {
534
394
  // ============================================================
535
395
 
536
396
  async function main() {
537
- // Build config - only enable channels when explicitly configured
538
397
  const config: StackConfig = {
539
- // SSH only enabled if SSH_CHAT_DIR is set
540
398
  ...(process.env.SSH_CHAT_DIR ? {
541
399
  ssh: {
542
400
  chatDir: process.env.SSH_CHAT_DIR,
543
401
  pollInterval: parseInt(process.env.SSH_POLL_INTERVAL || "500", 10),
544
402
  }
545
403
  } : {}),
546
- // Telegram only enabled if bot token is set
547
404
  ...(process.env.TELEGRAM_BOT_TOKEN ? {
548
405
  telegram: {
549
406
  botToken: process.env.TELEGRAM_BOT_TOKEN,
550
407
  allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined,
551
408
  }
552
409
  } : {}),
553
- // API enabled by default
554
410
  api: {
555
411
  port: parseInt(process.env.API_PORT || "8911", 10),
556
412
  host: process.env.API_HOST,
557
413
  },
558
- ralph: {
559
- worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
560
- repoUrl: process.env.REPO_URL || "",
561
- },
562
414
  node: {
563
415
  name: process.env.NODE_NAME || "stack",
564
416
  hostname: process.env.HOSTNAME || "localhost",
@@ -567,7 +419,6 @@ async function main() {
567
419
 
568
420
  const stack = new Stack(config);
569
421
 
570
- // Handle shutdown
571
422
  process.on("SIGINT", async () => {
572
423
  await stack.stop();
573
424
  process.exit(0);
@@ -581,7 +432,6 @@ async function main() {
581
432
  await stack.start();
582
433
  }
583
434
 
584
- // Run if executed directly
585
435
  if (import.meta.main) {
586
436
  main().catch((error) => {
587
437
  console.error("[Stack] Fatal error:", error);