@ebowwa/stack 0.1.1 → 0.1.3

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 (3) hide show
  1. package/dist/index.js +112 -28
  2. package/package.json +1 -1
  3. package/src/index.ts +143 -35
package/dist/index.js CHANGED
@@ -52387,7 +52387,7 @@ ${taggedContent}`
52387
52387
  }
52388
52388
  function parseMemoryCommand(memory, channel, message) {
52389
52389
  const trimmed = message.trim();
52390
- const isMemoryCommand = trimmed.startsWith("/memory ") || trimmed.startsWith("!memory ");
52390
+ const isMemoryCommand = trimmed === "/memory" || trimmed === "!memory" || trimmed.startsWith("/memory ") || trimmed.startsWith("!memory ");
52391
52391
  if (!isMemoryCommand) {
52392
52392
  return { handled: false };
52393
52393
  }
@@ -57408,8 +57408,7 @@ class Stack {
57408
57408
  abortController = null;
57409
57409
  constructor(config) {
57410
57410
  this.config = {
57411
- ssh: config.ssh ?? { chatDir: "/root/.ssh-chat" },
57412
- telegram: config.telegram ?? {},
57411
+ ...config,
57413
57412
  api: config.api ?? { port: 8911 },
57414
57413
  ralph: config.ralph ?? { worktreesDir: "/root/worktrees", repoUrl: "" },
57415
57414
  ai: config.ai ?? { model: "GLM-4.7", temperature: 0.7, maxTokens: 4096 },
@@ -57418,27 +57417,37 @@ class Stack {
57418
57417
  this.state = {
57419
57418
  started: new Date,
57420
57419
  channels: { ssh: false, telegram: false },
57421
- api: { enabled: !!this.config.api },
57420
+ api: { enabled: !!this.config.api, port: this.config.api?.port },
57422
57421
  ralphLoops: new Map
57423
57422
  };
57423
+ const memoryChannels = {};
57424
+ const permissions = {};
57425
+ const enabledChannels = [];
57426
+ if (this.config.ssh) {
57427
+ memoryChannels.ssh = { memoryFile: `${this.config.ssh.chatDir}/memory.json`, maxMessages: 50 };
57428
+ enabledChannels.push("ssh");
57429
+ }
57430
+ if (this.config.telegram) {
57431
+ memoryChannels.telegram = { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 };
57432
+ enabledChannels.push("telegram");
57433
+ }
57434
+ if (this.config.api) {
57435
+ memoryChannels.api = { memoryFile: "/root/.api-memory.json", maxMessages: 100 };
57436
+ enabledChannels.push("api");
57437
+ }
57438
+ for (const channel of enabledChannels) {
57439
+ permissions[channel] = { canRead: enabledChannels.filter((c) => c !== channel) };
57440
+ }
57424
57441
  this.memory = createPermissionMemory({
57425
- channels: {
57426
- ssh: { memoryFile: `${this.config.ssh.chatDir}/memory.json`, maxMessages: 50 },
57427
- telegram: { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 },
57428
- api: { memoryFile: "/root/.api-memory.json", maxMessages: 100 }
57429
- },
57430
- permissions: {
57431
- ssh: { canRead: ["telegram", "api"] },
57432
- telegram: { canRead: ["ssh", "api"] },
57433
- api: { canRead: ["ssh", "telegram"] }
57434
- }
57442
+ channels: memoryChannels,
57443
+ permissions
57435
57444
  });
57436
57445
  this.router = createChannelRouter({
57437
57446
  announcement: {
57438
57447
  serverName: this.config.node.name,
57439
57448
  hostname: this.config.node.hostname,
57440
57449
  packageName: "@ebowwa/stack",
57441
- version: "0.1.0"
57450
+ version: "0.1.1"
57442
57451
  }
57443
57452
  });
57444
57453
  this.client = new GLMClient;
@@ -57610,7 +57619,71 @@ Prompt: ${prompt.slice(0, 100)}...`;
57610
57619
  if (!this.config.api)
57611
57620
  return;
57612
57621
  const port = this.config.api.port ?? 8911;
57613
- console.log(`[Stack] API would start on :${port} (implement with Bun.serve)`);
57622
+ const host = this.config.api.host ?? "0.0.0.0";
57623
+ const server = Bun.serve({
57624
+ port,
57625
+ host,
57626
+ fetch: async (req) => {
57627
+ const url = new URL(req.url);
57628
+ const path = url.pathname;
57629
+ const corsHeaders = {
57630
+ "Access-Control-Allow-Origin": "*",
57631
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
57632
+ "Access-Control-Allow-Headers": "Content-Type"
57633
+ };
57634
+ if (req.method === "OPTIONS") {
57635
+ return new Response(null, { headers: corsHeaders });
57636
+ }
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 });
57665
+ }
57666
+ }
57667
+ });
57668
+ console.log(`[Stack] API running on http://${host}:${port}`);
57669
+ }
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());
57614
57687
  }
57615
57688
  async start() {
57616
57689
  console.log(`[Stack] Starting ${this.config.node.name}...`);
@@ -57621,10 +57694,15 @@ Prompt: ${prompt.slice(0, 100)}...`;
57621
57694
  await this.router.start();
57622
57695
  this.startAPI();
57623
57696
  console.log("[Stack] Running!");
57624
- console.log(` - SSH: echo 'msg' > ${this.config.ssh.chatDir}/in`);
57697
+ if (this.state.channels.ssh) {
57698
+ console.log(` - SSH: echo 'msg' > ${this.config.ssh.chatDir}/in`);
57699
+ }
57625
57700
  if (this.state.channels.telegram) {
57626
57701
  console.log(" - Telegram: enabled");
57627
57702
  }
57703
+ if (this.state.api.enabled) {
57704
+ console.log(` - API: :${this.state.api.port}`);
57705
+ }
57628
57706
  console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
57629
57707
  await new Promise(() => {});
57630
57708
  }
@@ -57640,17 +57718,22 @@ Prompt: ${prompt.slice(0, 100)}...`;
57640
57718
  }
57641
57719
  }
57642
57720
  async function main() {
57643
- const stack = new Stack({
57644
- ssh: {
57645
- chatDir: process.env.SSH_CHAT_DIR || "/root/.ssh-chat",
57646
- pollInterval: 500
57647
- },
57648
- telegram: {
57649
- botToken: process.env.TELEGRAM_BOT_TOKEN,
57650
- allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined
57651
- },
57721
+ const config = {
57722
+ ...process.env.SSH_CHAT_DIR ? {
57723
+ ssh: {
57724
+ chatDir: process.env.SSH_CHAT_DIR,
57725
+ pollInterval: parseInt(process.env.SSH_POLL_INTERVAL || "500", 10)
57726
+ }
57727
+ } : {},
57728
+ ...process.env.TELEGRAM_BOT_TOKEN ? {
57729
+ telegram: {
57730
+ botToken: process.env.TELEGRAM_BOT_TOKEN,
57731
+ allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined
57732
+ }
57733
+ } : {},
57652
57734
  api: {
57653
- port: parseInt(process.env.API_PORT || "8911", 10)
57735
+ port: parseInt(process.env.API_PORT || "8911", 10),
57736
+ host: process.env.API_HOST
57654
57737
  },
57655
57738
  ralph: {
57656
57739
  worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
@@ -57660,7 +57743,8 @@ async function main() {
57660
57743
  name: process.env.NODE_NAME || "stack",
57661
57744
  hostname: process.env.HOSTNAME || "localhost"
57662
57745
  }
57663
- });
57746
+ };
57747
+ const stack = new Stack(config);
57664
57748
  process.on("SIGINT", async () => {
57665
57749
  await stack.stop();
57666
57750
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebowwa/stack",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Full-stack daemon orchestrator combining unified-router (cross-channel) and node-agent (Ralph orchestration)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -94,7 +94,7 @@ interface RalphLoopInfo {
94
94
  // ============================================================
95
95
 
96
96
  export class Stack {
97
- private config: Required<StackConfig>;
97
+ private config: StackConfig;
98
98
  private state: StackState;
99
99
  private router: ReturnType<typeof createChannelRouter>;
100
100
  private memory: ReturnType<typeof createPermissionMemory>;
@@ -104,34 +104,50 @@ export class Stack {
104
104
  private abortController: AbortController | null = null;
105
105
 
106
106
  constructor(config: StackConfig) {
107
+ // Only set defaults for non-channel config
107
108
  this.config = {
108
- ssh: config.ssh ?? { chatDir: "/root/.ssh-chat" },
109
- telegram: config.telegram ?? {},
109
+ ...config,
110
110
  api: config.api ?? { port: 8911 },
111
111
  ralph: config.ralph ?? { worktreesDir: "/root/worktrees", repoUrl: "" },
112
112
  ai: config.ai ?? { model: "GLM-4.7", temperature: 0.7, maxTokens: 4096 },
113
113
  node: config.node ?? { name: "stack", hostname: "localhost" },
114
+ // ssh and telegram remain undefined if not provided
114
115
  };
115
116
 
116
117
  this.state = {
117
118
  started: new Date(),
118
119
  channels: { ssh: false, telegram: false },
119
- api: { enabled: !!this.config.api },
120
+ api: { enabled: !!this.config.api, port: this.config.api?.port },
120
121
  ralphLoops: new Map(),
121
122
  };
122
123
 
123
- // Initialize shared memory
124
+ // Build memory channels dynamically based on enabled channels
125
+ const memoryChannels: Record<string, { memoryFile: string; maxMessages: number }> = {};
126
+ const permissions: Record<string, { canRead: string[] }> = {};
127
+ const enabledChannels: string[] = [];
128
+
129
+ if (this.config.ssh) {
130
+ memoryChannels.ssh = { memoryFile: `${this.config.ssh.chatDir}/memory.json`, maxMessages: 50 };
131
+ enabledChannels.push("ssh");
132
+ }
133
+ if (this.config.telegram) {
134
+ memoryChannels.telegram = { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 };
135
+ enabledChannels.push("telegram");
136
+ }
137
+ if (this.config.api) {
138
+ memoryChannels.api = { memoryFile: "/root/.api-memory.json", maxMessages: 100 };
139
+ enabledChannels.push("api");
140
+ }
141
+
142
+ // Set up cross-channel permissions for enabled channels
143
+ for (const channel of enabledChannels) {
144
+ permissions[channel] = { canRead: enabledChannels.filter(c => c !== channel) };
145
+ }
146
+
147
+ // Initialize shared memory (empty if no channels)
124
148
  this.memory = createPermissionMemory({
125
- channels: {
126
- ssh: { memoryFile: `${this.config.ssh.chatDir}/memory.json`, maxMessages: 50 },
127
- telegram: { memoryFile: "/root/.telegram-memory.json", maxMessages: 50 },
128
- api: { memoryFile: "/root/.api-memory.json", maxMessages: 100 },
129
- },
130
- permissions: {
131
- ssh: { canRead: ["telegram", "api"] },
132
- telegram: { canRead: ["ssh", "api"] },
133
- api: { canRead: ["ssh", "telegram"] },
134
- },
149
+ channels: memoryChannels,
150
+ permissions,
135
151
  });
136
152
 
137
153
  // Initialize router
@@ -140,7 +156,7 @@ export class Stack {
140
156
  serverName: this.config.node.name,
141
157
  hostname: this.config.node.hostname,
142
158
  packageName: "@ebowwa/stack",
143
- version: "0.1.0",
159
+ version: "0.1.1",
144
160
  },
145
161
  });
146
162
 
@@ -351,20 +367,96 @@ export class Stack {
351
367
  }
352
368
 
353
369
  // ============================================================
354
- // HTTP API (optional, delegates to node-agent)
370
+ // HTTP API
355
371
  // ============================================================
356
372
 
357
373
  private startAPI(): void {
358
374
  if (!this.config.api) return;
359
375
 
360
376
  const port = this.config.api.port ?? 8911;
361
- console.log(`[Stack] API would start on :${port} (implement with Bun.serve)`);
377
+ const host = this.config.api.host ?? "0.0.0.0";
378
+
379
+ const server = Bun.serve({
380
+ port,
381
+ host,
382
+ fetch: async (req) => {
383
+ const url = new URL(req.url);
384
+ const path = url.pathname;
385
+
386
+ // CORS headers
387
+ const corsHeaders = {
388
+ "Access-Control-Allow-Origin": "*",
389
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
390
+ "Access-Control-Allow-Headers": "Content-Type",
391
+ };
392
+
393
+ if (req.method === "OPTIONS") {
394
+ return new Response(null, { headers: corsHeaders });
395
+ }
396
+
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 });
435
+ }
436
+ },
437
+ });
438
+
439
+ console.log(`[Stack] API running on http://${host}:${port}`);
440
+ }
441
+
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,
452
+ },
453
+ ralphLoops: this.state.ralphLoops.size,
454
+ uptime: Math.floor((Date.now() - this.state.started.getTime()) / 1000),
455
+ };
456
+ }
362
457
 
363
- // TODO: Start HTTP server with routes:
364
- // GET /api/status - Stack status
365
- // GET /api/ralph-loops - List Ralph loops
366
- // POST /api/ralph-loops - Start Ralph loop
367
- // DELETE /api/ralph-loops/:id - Stop Ralph loop
458
+ private listRalphLoopsJSON(): object[] {
459
+ return Array.from(this.state.ralphLoops.values());
368
460
  }
369
461
 
370
462
  // ============================================================
@@ -376,7 +468,7 @@ export class Stack {
376
468
 
377
469
  this.abortController = new AbortController();
378
470
 
379
- // Register channels
471
+ // Register channels (only if configured)
380
472
  await this.registerSSH();
381
473
  await this.registerTelegram();
382
474
 
@@ -390,10 +482,15 @@ export class Stack {
390
482
  this.startAPI();
391
483
 
392
484
  console.log("[Stack] Running!");
393
- console.log(` - SSH: echo 'msg' > ${this.config.ssh.chatDir}/in`);
485
+ if (this.state.channels.ssh) {
486
+ console.log(` - SSH: echo 'msg' > ${this.config.ssh!.chatDir}/in`);
487
+ }
394
488
  if (this.state.channels.telegram) {
395
489
  console.log(" - Telegram: enabled");
396
490
  }
491
+ if (this.state.api.enabled) {
492
+ console.log(` - API: :${this.state.api.port}`);
493
+ }
397
494
  console.log(" - Commands: /ralph start|list|stop, /status, /memory <cmd>");
398
495
 
399
496
  // Keep running
@@ -420,17 +517,26 @@ export class Stack {
420
517
  // ============================================================
421
518
 
422
519
  async function main() {
423
- const stack = new Stack({
424
- ssh: {
425
- chatDir: process.env.SSH_CHAT_DIR || "/root/.ssh-chat",
426
- pollInterval: 500,
427
- },
428
- telegram: {
429
- botToken: process.env.TELEGRAM_BOT_TOKEN,
430
- allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined,
431
- },
520
+ // Build config - only enable channels when explicitly configured
521
+ const config: StackConfig = {
522
+ // SSH only enabled if SSH_CHAT_DIR is set
523
+ ...(process.env.SSH_CHAT_DIR ? {
524
+ ssh: {
525
+ chatDir: process.env.SSH_CHAT_DIR,
526
+ pollInterval: parseInt(process.env.SSH_POLL_INTERVAL || "500", 10),
527
+ }
528
+ } : {}),
529
+ // Telegram only enabled if bot token is set
530
+ ...(process.env.TELEGRAM_BOT_TOKEN ? {
531
+ telegram: {
532
+ botToken: process.env.TELEGRAM_BOT_TOKEN,
533
+ allowedChats: process.env.TELEGRAM_CHAT_ID ? [parseInt(process.env.TELEGRAM_CHAT_ID, 10)] : undefined,
534
+ }
535
+ } : {}),
536
+ // API enabled by default
432
537
  api: {
433
538
  port: parseInt(process.env.API_PORT || "8911", 10),
539
+ host: process.env.API_HOST,
434
540
  },
435
541
  ralph: {
436
542
  worktreesDir: process.env.WORKTREES_DIR || "/root/worktrees",
@@ -440,7 +546,9 @@ async function main() {
440
546
  name: process.env.NODE_NAME || "stack",
441
547
  hostname: process.env.HOSTNAME || "localhost",
442
548
  },
443
- });
549
+ };
550
+
551
+ const stack = new Stack(config);
444
552
 
445
553
  // Handle shutdown
446
554
  process.on("SIGINT", async () => {