@ebowwa/stack 0.1.3 → 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 +39 -136
  3. package/package.json +7 -10
  4. package/src/index.ts +66 -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;
@@ -57488,17 +57482,28 @@ class Stack {
57488
57482
  const channel = channelId.platform;
57489
57483
  const text = message.text;
57490
57484
  console.log(`[${channel}] ${text.slice(0, 50)}...`);
57485
+ const telegramChannel = this.channels.get("telegram");
57486
+ const chatId = channelId.metadata?.chatId;
57487
+ if (channel === "telegram" && telegramChannel?.startTypingIndicator && chatId) {
57488
+ telegramChannel.startTypingIndicator(chatId);
57489
+ }
57490
+ const stopTyping = () => {
57491
+ if (channel === "telegram" && telegramChannel?.stopTypingIndicator && chatId) {
57492
+ telegramChannel.stopTypingIndicator(chatId);
57493
+ }
57494
+ };
57491
57495
  const cmdResult = parseMemoryCommand(this.memory, channel, text);
57492
57496
  if (cmdResult.handled) {
57497
+ stopTyping();
57493
57498
  return {
57494
57499
  content: { text: cmdResult.response || "Done" },
57495
57500
  replyTo: { messageId: message.messageId, channelId: message.channelId }
57496
57501
  };
57497
57502
  }
57498
- const ralphResult = await this.handleRalphCommand(channel, text);
57499
- if (ralphResult) {
57503
+ if (text.trim().toLowerCase() === "/status") {
57504
+ stopTyping();
57500
57505
  return {
57501
- content: { text: ralphResult },
57506
+ content: { text: this.getStatus() },
57502
57507
  replyTo: { messageId: message.messageId, channelId: message.channelId }
57503
57508
  };
57504
57509
  }
@@ -57512,12 +57517,14 @@ class Stack {
57512
57517
  maxTokens: this.config.ai.maxTokens
57513
57518
  });
57514
57519
  this.memory.addMessage(channel, { role: "assistant", content: result.content });
57520
+ stopTyping();
57515
57521
  return {
57516
57522
  content: { text: result.content },
57517
57523
  replyTo: { messageId: message.messageId, channelId: message.channelId }
57518
57524
  };
57519
57525
  } catch (error) {
57520
57526
  const errorMsg = error instanceof Error ? error.message : String(error);
57527
+ stopTyping();
57521
57528
  return {
57522
57529
  content: { text: `Error: ${errorMsg}` },
57523
57530
  replyTo: { messageId: message.messageId, channelId: message.channelId }
@@ -57525,91 +57532,27 @@ class Stack {
57525
57532
  }
57526
57533
  }
57527
57534
  buildSystemPrompt() {
57528
- 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.
57529
57537
 
57530
- ## What You Manage
57531
- - **Ralph Loops**: Autonomous AI agents running tasks
57532
- - **Git Worktrees**: Isolated development environments
57533
- - **Node Monitoring**: CPU, memory, disk usage
57534
- - **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
57535
57542
 
57536
57543
  ## Commands
57537
- - \`/ralph start <prompt>\` \u2014 Start a Ralph loop
57538
- - \`/ralph list\` \u2014 List running loops
57539
- - \`/ralph stop <id>\` \u2014 Stop a loop
57540
57544
  - \`/status\` \u2014 Node status
57541
57545
  - \`/memory <cmd>\` \u2014 Memory management (grant, revoke, list, clear)
57542
57546
 
57543
- ## Stack Info
57547
+ ## Info
57544
57548
  - Node: ${this.config.node.name}
57545
- - Channels: ${Object.entries(this.state.channels).filter(([, v]) => v).map(([k]) => k).join(", ") || "none"}
57549
+ - Channels: ${channelList}
57546
57550
  - API: :${this.config.api?.port ?? 8911}`;
57547
57551
  }
57548
- async handleRalphCommand(channel, text) {
57549
- const parts = text.trim().split(/\s+/);
57550
- const cmd = parts[0].toLowerCase();
57551
- if (cmd === "/ralph" || cmd === "/ralph") {
57552
- const subCmd = parts[1]?.toLowerCase();
57553
- switch (subCmd) {
57554
- case "start": {
57555
- const prompt = parts.slice(2).join(" ");
57556
- if (!prompt)
57557
- return "Usage: /ralph start <prompt>";
57558
- return await this.startRalphLoop(prompt);
57559
- }
57560
- case "list":
57561
- return this.listRalphLoops();
57562
- case "stop": {
57563
- const id = parts[2];
57564
- if (!id)
57565
- return "Usage: /ralph stop <id>";
57566
- return await this.stopRalphLoop(id);
57567
- }
57568
- default:
57569
- return "Ralph commands: start, list, stop";
57570
- }
57571
- }
57572
- if (cmd === "/status") {
57573
- return this.getStatus();
57574
- }
57575
- return null;
57576
- }
57577
- async startRalphLoop(prompt) {
57578
- const id = `ralph-${Date.now()}`;
57579
- this.state.ralphLoops.set(id, {
57580
- id,
57581
- worktree: `${this.config.ralph.worktreesDir}/${id}`,
57582
- prompt,
57583
- status: "running",
57584
- iterations: 0,
57585
- started: new Date
57586
- });
57587
- return `Started Ralph loop: ${id}
57588
- Prompt: ${prompt.slice(0, 100)}...`;
57589
- }
57590
- listRalphLoops() {
57591
- if (this.state.ralphLoops.size === 0) {
57592
- return "No Ralph loops running";
57593
- }
57594
- const lines = ["Ralph Loops:"];
57595
- for (const [id, info] of this.state.ralphLoops) {
57596
- lines.push(` ${id}: ${info.status} (${info.iterations} iters)`);
57597
- }
57598
- return lines.join(`
57599
- `);
57600
- }
57601
- async stopRalphLoop(id) {
57602
- const info = this.state.ralphLoops.get(id);
57603
- if (!info)
57604
- return `Ralph loop not found: ${id}`;
57605
- info.status = "paused";
57606
- return `Stopped Ralph loop: ${id}`;
57607
- }
57608
57552
  getStatus() {
57609
57553
  const lines = [
57610
57554
  `**${this.config.node.name} Status**`,
57611
57555
  `Channels: SSH=${this.state.channels.ssh}, Telegram=${this.state.channels.telegram}`,
57612
- `Ralph Loops: ${this.state.ralphLoops.size}`,
57613
57556
  `Uptime: ${Math.floor((Date.now() - this.state.started.getTime()) / 1000)}s`
57614
57557
  ];
57615
57558
  return lines.join(`
@@ -57620,7 +57563,7 @@ Prompt: ${prompt.slice(0, 100)}...`;
57620
57563
  return;
57621
57564
  const port = this.config.api.port ?? 8911;
57622
57565
  const host = this.config.api.host ?? "0.0.0.0";
57623
- const server = Bun.serve({
57566
+ Bun.serve({
57624
57567
  port,
57625
57568
  host,
57626
57569
  fetch: async (req) => {
@@ -57628,63 +57571,27 @@ Prompt: ${prompt.slice(0, 100)}...`;
57628
57571
  const path = url.pathname;
57629
57572
  const corsHeaders = {
57630
57573
  "Access-Control-Allow-Origin": "*",
57631
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
57574
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
57632
57575
  "Access-Control-Allow-Headers": "Content-Type"
57633
57576
  };
57634
57577
  if (req.method === "OPTIONS") {
57635
57578
  return new Response(null, { headers: corsHeaders });
57636
57579
  }
57637
- try {
57638
- if (path === "/api/status" && req.method === "GET") {
57639
- return Response.json(this.getStatusJSON(), { headers: corsHeaders });
57640
- }
57641
- if (path === "/api/ralph-loops" && req.method === "GET") {
57642
- return Response.json(this.listRalphLoopsJSON(), { headers: corsHeaders });
57643
- }
57644
- if (path === "/api/ralph-loops" && req.method === "POST") {
57645
- const body = await req.json();
57646
- const prompt = body.prompt;
57647
- if (!prompt) {
57648
- return Response.json({ error: "Missing prompt" }, { status: 400, headers: corsHeaders });
57649
- }
57650
- const result = await this.startRalphLoop(prompt);
57651
- return Response.json({ message: result }, { headers: corsHeaders });
57652
- }
57653
- const match = path.match(/^\/api\/ralph-loops\/(.+)$/);
57654
- if (match && req.method === "DELETE") {
57655
- const result = await this.stopRalphLoop(match[1]);
57656
- return Response.json({ message: result }, { headers: corsHeaders });
57657
- }
57658
- if (path === "/health") {
57659
- return Response.json({ status: "ok" }, { headers: corsHeaders });
57660
- }
57661
- return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
57662
- } catch (error) {
57663
- const errorMsg = error instanceof Error ? error.message : String(error);
57664
- 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 });
57586
+ }
57587
+ if (path === "/health") {
57588
+ return Response.json({ status: "ok" }, { headers: corsHeaders });
57665
57589
  }
57590
+ return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
57666
57591
  }
57667
57592
  });
57668
57593
  console.log(`[Stack] API running on http://${host}:${port}`);
57669
57594
  }
57670
- getStatusJSON() {
57671
- return {
57672
- node: this.config.node.name,
57673
- channels: {
57674
- ssh: this.state.channels.ssh,
57675
- telegram: this.state.channels.telegram
57676
- },
57677
- api: {
57678
- enabled: this.state.api.enabled,
57679
- port: this.state.api.port
57680
- },
57681
- ralphLoops: this.state.ralphLoops.size,
57682
- uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000)
57683
- };
57684
- }
57685
- listRalphLoopsJSON() {
57686
- return Array.from(this.state.ralphLoops.values());
57687
- }
57688
57595
  async start() {
57689
57596
  console.log(`[Stack] Starting ${this.config.node.name}...`);
57690
57597
  this.abortController = new AbortController;
@@ -57703,7 +57610,7 @@ Prompt: ${prompt.slice(0, 100)}...`;
57703
57610
  if (this.state.api.enabled) {
57704
57611
  console.log(` - API: :${this.state.api.port}`);
57705
57612
  }
57706
- console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
57613
+ console.log(" - Commands: /status, /memory <cmd>");
57707
57614
  await new Promise(() => {});
57708
57615
  }
57709
57616
  async stop() {
@@ -57735,10 +57642,6 @@ async function main() {
57735
57642
  port: parseInt(process.env.API_PORT || "8911", 10),
57736
57643
  host: process.env.API_HOST
57737
57644
  },
57738
- ralph: {
57739
- worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
57740
- repoUrl: process.env.REPO_URL || ""
57741
- },
57742
57645
  node: {
57743
57646
  name: process.env.NODE_NAME || "stack",
57744
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.3",
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,25 +189,39 @@ 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)}...`);
215
196
 
197
+ // Start typing indicator for Telegram
198
+ const telegramChannel = this.channels.get("telegram") as { startTypingIndicator?: (chatId: string) => void; stopTypingIndicator?: (chatId: string) => void } | undefined;
199
+ const chatId = channelId.metadata?.chatId as string | undefined;
200
+ if (channel === "telegram" && telegramChannel?.startTypingIndicator && chatId) {
201
+ telegramChannel.startTypingIndicator(chatId);
202
+ }
203
+
204
+ const stopTyping = () => {
205
+ if (channel === "telegram" && telegramChannel?.stopTypingIndicator && chatId) {
206
+ telegramChannel.stopTypingIndicator(chatId);
207
+ }
208
+ };
209
+
216
210
  // Check for memory commands
217
211
  const cmdResult = parseMemoryCommand(this.memory, channel, text);
218
212
  if (cmdResult.handled) {
213
+ stopTyping();
219
214
  return {
220
215
  content: { text: cmdResult.response || "Done" },
221
216
  replyTo: { messageId: message.messageId, channelId: message.channelId },
222
217
  };
223
218
  }
224
219
 
225
- // Check for Ralph commands
226
- const ralphResult = await this.handleRalphCommand(channel, text);
227
- if (ralphResult) {
220
+ // Check for status command
221
+ if (text.trim().toLowerCase() === "/status") {
222
+ stopTyping();
228
223
  return {
229
- content: { text: ralphResult },
224
+ content: { text: this.getStatus() },
230
225
  replyTo: { messageId: message.messageId, channelId: message.channelId },
231
226
  };
232
227
  }
@@ -252,6 +247,7 @@ export class Stack {
252
247
  });
253
248
 
254
249
  this.memory.addMessage(channel, { role: "assistant", content: result.content });
250
+ stopTyping();
255
251
 
256
252
  return {
257
253
  content: { text: result.content },
@@ -259,6 +255,7 @@ export class Stack {
259
255
  };
260
256
  } catch (error) {
261
257
  const errorMsg = error instanceof Error ? error.message : String(error);
258
+ stopTyping();
262
259
  return {
263
260
  content: { text: `Error: ${errorMsg}` },
264
261
  replyTo: { messageId: message.messageId, channelId: message.channelId },
@@ -267,107 +264,39 @@ export class Stack {
267
264
  }
268
265
 
269
266
  private buildSystemPrompt(): string {
270
- 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.
271
273
 
272
- ## What You Manage
273
- - **Ralph Loops**: Autonomous AI agents running tasks
274
- - **Git Worktrees**: Isolated development environments
275
- - **Node Monitoring**: CPU, memory, disk usage
276
- - **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
277
278
 
278
279
  ## Commands
279
- - \`/ralph start <prompt>\` — Start a Ralph loop
280
- - \`/ralph list\` — List running loops
281
- - \`/ralph stop <id>\` — Stop a loop
282
280
  - \`/status\` — Node status
283
281
  - \`/memory <cmd>\` — Memory management (grant, revoke, list, clear)
284
282
 
285
- ## Stack Info
283
+ ## Info
286
284
  - Node: ${this.config.node.name}
287
- - Channels: ${Object.entries(this.state.channels).filter(([, v]) => v).map(([k]) => k).join(", ") || "none"}
285
+ - Channels: ${channelList}
288
286
  - API: :${this.config.api?.port ?? 8911}`;
289
287
  }
290
288
 
291
- // ============================================================
292
- // Ralph Loop Management (delegates to node-agent)
293
- // ============================================================
294
-
295
- private async handleRalphCommand(channel: string, text: string): Promise<string | null> {
296
- const parts = text.trim().split(/\s+/);
297
- const cmd = parts[0].toLowerCase();
298
-
299
- if (cmd === "/ralph" || cmd === "/ralph") {
300
- const subCmd = parts[1]?.toLowerCase();
301
-
302
- switch (subCmd) {
303
- case "start": {
304
- const prompt = parts.slice(2).join(" ");
305
- if (!prompt) return "Usage: /ralph start <prompt>";
306
- return await this.startRalphLoop(prompt);
307
- }
308
- case "list":
309
- return this.listRalphLoops();
310
- case "stop": {
311
- const id = parts[2];
312
- if (!id) return "Usage: /ralph stop <id>";
313
- return await this.stopRalphLoop(id);
314
- }
315
- default:
316
- return "Ralph commands: start, list, stop";
317
- }
318
- }
319
-
320
- if (cmd === "/status") {
321
- return this.getStatus();
322
- }
323
-
324
- return null;
325
- }
326
-
327
- private async startRalphLoop(prompt: string): Promise<string> {
328
- // TODO: Delegate to @ebowwa/node-agent RalphService
329
- const id = `ralph-${Date.now()}`;
330
- this.state.ralphLoops.set(id, {
331
- id,
332
- worktree: `${this.config.ralph.worktreesDir}/${id}`,
333
- prompt,
334
- status: "running",
335
- iterations: 0,
336
- started: new Date(),
337
- });
338
- return `Started Ralph loop: ${id}\nPrompt: ${prompt.slice(0, 100)}...`;
339
- }
340
-
341
- private listRalphLoops(): string {
342
- if (this.state.ralphLoops.size === 0) {
343
- return "No Ralph loops running";
344
- }
345
- const lines = ["Ralph Loops:"];
346
- for (const [id, info] of this.state.ralphLoops) {
347
- lines.push(` ${id}: ${info.status} (${info.iterations} iters)`);
348
- }
349
- return lines.join("\n");
350
- }
351
-
352
- private async stopRalphLoop(id: string): Promise<string> {
353
- const info = this.state.ralphLoops.get(id);
354
- if (!info) return `Ralph loop not found: ${id}`;
355
- info.status = "paused";
356
- return `Stopped Ralph loop: ${id}`;
357
- }
358
-
359
289
  private getStatus(): string {
360
290
  const lines = [
361
291
  `**${this.config.node.name} Status**`,
362
292
  `Channels: SSH=${this.state.channels.ssh}, Telegram=${this.state.channels.telegram}`,
363
- `Ralph Loops: ${this.state.ralphLoops.size}`,
364
293
  `Uptime: ${Math.floor((Date.now() - this.state.started.getTime()) / 1000)}s`,
365
294
  ];
366
295
  return lines.join("\n");
367
296
  }
368
297
 
369
298
  // ============================================================
370
- // HTTP API
299
+ // HTTP API (minimal)
371
300
  // ============================================================
372
301
 
373
302
  private startAPI(): void {
@@ -376,17 +305,16 @@ export class Stack {
376
305
  const port = this.config.api.port ?? 8911;
377
306
  const host = this.config.api.host ?? "0.0.0.0";
378
307
 
379
- const server = Bun.serve({
308
+ Bun.serve({
380
309
  port,
381
310
  host,
382
311
  fetch: async (req) => {
383
312
  const url = new URL(req.url);
384
313
  const path = url.pathname;
385
314
 
386
- // CORS headers
387
315
  const corsHeaders = {
388
316
  "Access-Control-Allow-Origin": "*",
389
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
317
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
390
318
  "Access-Control-Allow-Headers": "Content-Type",
391
319
  };
392
320
 
@@ -394,69 +322,25 @@ export class Stack {
394
322
  return new Response(null, { headers: corsHeaders });
395
323
  }
396
324
 
397
- try {
398
- // GET /api/status
399
- if (path === "/api/status" && req.method === "GET") {
400
- return Response.json(this.getStatusJSON(), { headers: corsHeaders });
401
- }
402
-
403
- // GET /api/ralph-loops
404
- if (path === "/api/ralph-loops" && req.method === "GET") {
405
- return Response.json(this.listRalphLoopsJSON(), { headers: corsHeaders });
406
- }
407
-
408
- // POST /api/ralph-loops
409
- if (path === "/api/ralph-loops" && req.method === "POST") {
410
- const body = await req.json();
411
- const prompt = body.prompt;
412
- if (!prompt) {
413
- return Response.json({ error: "Missing prompt" }, { status: 400, headers: corsHeaders });
414
- }
415
- const result = await this.startRalphLoop(prompt);
416
- return Response.json({ message: result }, { headers: corsHeaders });
417
- }
418
-
419
- // DELETE /api/ralph-loops/:id
420
- const match = path.match(/^\/api\/ralph-loops\/(.+)$/);
421
- if (match && req.method === "DELETE") {
422
- const result = await this.stopRalphLoop(match[1]);
423
- return Response.json({ message: result }, { headers: corsHeaders });
424
- }
425
-
426
- // GET /health
427
- if (path === "/health") {
428
- return Response.json({ status: "ok" }, { headers: corsHeaders });
429
- }
430
-
431
- return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
432
- } catch (error) {
433
- const errorMsg = error instanceof Error ? error.message : String(error);
434
- 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 });
435
332
  }
436
- },
437
- });
438
333
 
439
- console.log(`[Stack] API running on http://${host}:${port}`);
440
- }
334
+ // GET /health
335
+ if (path === "/health") {
336
+ return Response.json({ status: "ok" }, { headers: corsHeaders });
337
+ }
441
338
 
442
- private getStatusJSON(): object {
443
- return {
444
- node: this.config.node.name,
445
- channels: {
446
- ssh: this.state.channels.ssh,
447
- telegram: this.state.channels.telegram,
448
- },
449
- api: {
450
- enabled: this.state.api.enabled,
451
- port: this.state.api.port,
339
+ return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
452
340
  },
453
- ralphLoops: this.state.ralphLoops.size,
454
- uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000),
455
- };
456
- }
341
+ });
457
342
 
458
- private listRalphLoopsJSON(): object[] {
459
- return Array.from(this.state.ralphLoops.values());
343
+ console.log(`[Stack] API running on http://${host}:${port}`);
460
344
  }
461
345
 
462
346
  // ============================================================
@@ -468,17 +352,12 @@ export class Stack {
468
352
 
469
353
  this.abortController = new AbortController();
470
354
 
471
- // Register channels (only if configured)
472
355
  await this.registerSSH();
473
356
  await this.registerTelegram();
474
357
 
475
- // Set handler
476
358
  this.router.setHandler((routed) => this.handleMessage(routed));
477
-
478
- // Start router
479
359
  await this.router.start();
480
360
 
481
- // Start API (optional)
482
361
  this.startAPI();
483
362
 
484
363
  console.log("[Stack] Running!");
@@ -491,10 +370,9 @@ export class Stack {
491
370
  if (this.state.api.enabled) {
492
371
  console.log(` - API: :${this.state.api.port}`);
493
372
  }
494
- console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
373
+ console.log(" - Commands: /status, /memory <cmd>");
495
374
 
496
- // Keep running
497
- await new Promise(() => {}); // Run forever
375
+ await new Promise(() => {});
498
376
  }
499
377
 
500
378
  async stop(): Promise<void> {
@@ -502,7 +380,6 @@ export class Stack {
502
380
  this.abortController?.abort();
503
381
  await this.router.stop();
504
382
 
505
- // Stop all channels
506
383
  for (const [name, channel] of this.channels) {
507
384
  console.log(`[Stack] Stopping ${name}...`);
508
385
  await channel.stop();
@@ -517,31 +394,23 @@ export class Stack {
517
394
  // ============================================================
518
395
 
519
396
  async function main() {
520
- // Build config - only enable channels when explicitly configured
521
397
  const config: StackConfig = {
522
- // SSH only enabled if SSH_CHAT_DIR is set
523
398
  ...(process.env.SSH_CHAT_DIR ? {
524
399
  ssh: {
525
400
  chatDir: process.env.SSH_CHAT_DIR,
526
401
  pollInterval: parseInt(process.env.SSH_POLL_INTERVAL || "500", 10),
527
402
  }
528
403
  } : {}),
529
- // Telegram only enabled if bot token is set
530
404
  ...(process.env.TELEGRAM_BOT_TOKEN ? {
531
405
  telegram: {
532
406
  botToken: process.env.TELEGRAM_BOT_TOKEN,
533
407
  allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined,
534
408
  }
535
409
  } : {}),
536
- // API enabled by default
537
410
  api: {
538
411
  port: parseInt(process.env.API_PORT || "8911", 10),
539
412
  host: process.env.API_HOST,
540
413
  },
541
- ralph: {
542
- worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
543
- repoUrl: process.env.REPO_URL || "",
544
- },
545
414
  node: {
546
415
  name: process.env.NODE_NAME || "stack",
547
416
  hostname: process.env.HOSTNAME || "localhost",
@@ -550,7 +419,6 @@ async function main() {
550
419
 
551
420
  const stack = new Stack(config);
552
421
 
553
- // Handle shutdown
554
422
  process.on("SIGINT", async () => {
555
423
  await stack.stop();
556
424
  process.exit(0);
@@ -564,7 +432,6 @@ async function main() {
564
432
  await stack.start();
565
433
  }
566
434
 
567
- // Run if executed directly
568
435
  if (import.meta.main) {
569
436
  main().catch((error) => {
570
437
  console.error("[Stack] Fatal error:", error);