@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.
- package/README.md +26 -11
- package/dist/index.js +25 -136
- package/package.json +7 -10
- package/src/index.ts +49 -199
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @ebowwa/stack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cross-channel AI stack with shared memory.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
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
|
-
#
|
|
22
|
-
bun run @ebowwa/stack
|
|
21
|
+
# Telegram only
|
|
22
|
+
TELEGRAM_BOT_TOKEN=xxx bun run @ebowwa/stack
|
|
23
23
|
|
|
24
|
-
#
|
|
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.
|
|
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
|
-
|
|
57510
|
-
if (ralphResult) {
|
|
57503
|
+
if (text.trim().toLowerCase() === "/status") {
|
|
57511
57504
|
stopTyping();
|
|
57512
57505
|
return {
|
|
57513
|
-
content: { text:
|
|
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
|
-
|
|
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
|
-
##
|
|
57545
|
-
-
|
|
57546
|
-
-
|
|
57547
|
-
-
|
|
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
|
-
##
|
|
57547
|
+
## Info
|
|
57558
57548
|
- Node: ${this.config.node.name}
|
|
57559
|
-
- Channels: ${
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
57652
|
-
|
|
57653
|
-
|
|
57654
|
-
|
|
57655
|
-
|
|
57656
|
-
|
|
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: /
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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 -
|
|
3
|
+
* @ebowwa/stack - Cross-Channel AI Stack
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Features:
|
|
6
6
|
* - Unified Router: Cross-channel communication (SSH + Telegram)
|
|
7
|
-
* -
|
|
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
|
-
* │ │
|
|
14
|
-
* │ │
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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"
|
|
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
|
|
240
|
-
|
|
241
|
-
if (ralphResult) {
|
|
220
|
+
// Check for status command
|
|
221
|
+
if (text.trim().toLowerCase() === "/status") {
|
|
242
222
|
stopTyping();
|
|
243
223
|
return {
|
|
244
|
-
content: { text:
|
|
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
|
-
|
|
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
|
-
##
|
|
290
|
-
-
|
|
291
|
-
-
|
|
292
|
-
-
|
|
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
|
-
##
|
|
283
|
+
## Info
|
|
303
284
|
- Node: ${this.config.node.name}
|
|
304
|
-
- Channels: ${
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
457
|
-
|
|
334
|
+
// GET /health
|
|
335
|
+
if (path === "/health") {
|
|
336
|
+
return Response.json({ status: "ok" }, { headers: corsHeaders });
|
|
337
|
+
}
|
|
458
338
|
|
|
459
|
-
|
|
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
|
-
|
|
471
|
-
uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000),
|
|
472
|
-
};
|
|
473
|
-
}
|
|
341
|
+
});
|
|
474
342
|
|
|
475
|
-
|
|
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: /
|
|
373
|
+
console.log(" - Commands: /status, /memory <cmd>");
|
|
512
374
|
|
|
513
|
-
|
|
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);
|