| 1 | // Sidecar boot: one process hosts the instance's default agents, each a normal |
| 2 | // E2EE member with its own identity on the /data disk (see runtime.mjs for the |
| 3 | // per-bot chat loop): |
| 4 | // |
| 5 | // Claude the plain assistant — replies in DMs, on @mention in groups |
| 6 | // (LCHAT_SESSION / LCHAT_INVITE; name via LCHAT_AGENT_NAME) |
| 7 | // Eleven the app guide — same chat loop with a shipped-in persona, plus a |
| 8 | // welcome DM to each new member (LCHAT_ELEVEN_SESSION / _INVITE) |
| 9 | // |
| 10 | // Each identity is optional: it runs if its session exists or its invite is set, |
| 11 | // so an instance can host any subset. (A third identity, the write-only |
| 12 | // Announcer, is retired: the server announces its own changelog now — a push |
| 13 | // to admins + the Admin app's update stream.) |
| 14 | import fs from "node:fs"; |
| 15 | import http from "node:http"; |
| 16 | import { BASE, AGENT_NAME, requireBase, loadSession } from "./config.mjs"; |
| 17 | import { provision } from "./provision.mjs"; |
| 18 | import { runBot } from "./runtime.mjs"; |
| 19 | import { CHIP_TOOLS } from "./actions.mjs"; |
| 20 | import { ELEVEN_NAME, ELEVEN_PERSONA, ELEVEN_DOCS, loadElevenSession } from "./eleven.mjs"; |
| 21 | |
| 22 | // Synchronous logging: async stdout to a pipe is buffered and lost if the |
| 23 | // container is killed (e.g. a failed health check), so write key lines with |
| 24 | // fs.writeSync so they always reach the logs. |
| 25 | const log = (m) => { |
| 26 | try { |
| 27 | fs.writeSync(1, `${m}\n`); |
| 28 | } catch {} |
| 29 | }; |
| 30 | process.on("uncaughtException", (e) => { |
| 31 | log(`uncaughtException: ${e?.stack || e}`); |
| 32 | process.exit(1); |
| 33 | }); |
| 34 | process.on("unhandledRejection", (e) => { |
| 35 | log(`unhandledRejection: ${e?.stack || e}`); |
| 36 | process.exit(1); |
| 37 | }); |
| 38 | log(`boot: sidecar starting, base=${BASE}`); |
| 39 | |
| 40 | requireBase(); |
| 41 | |
| 42 | // Optional health endpoint for container platforms that health-check over HTTP |
| 43 | // (e.g. Miren). Only bind it when PORT is explicitly set: the sidecar is a pure |
| 44 | // outbound WebSocket client, so under systemd nothing health-checks or routes to |
| 45 | // it — it needs no inbound listener, and binding a fixed port would collide when |
| 46 | // several sidecars share a host. |
| 47 | if (process.env.PORT) { |
| 48 | const PORT = Number(process.env.PORT); |
| 49 | http |
| 50 | .createServer((_req, res) => { |
| 51 | res.writeHead(200, { "content-type": "text/plain" }); |
| 52 | res.end("ok\n"); |
| 53 | }) |
| 54 | .listen(PORT, "0.0.0.0", () => log(`health listening on :${PORT}`)); |
| 55 | } |
| 56 | |
| 57 | // --- Claude: the plain assistant (the original sidecar bot) ---------------- |
| 58 | let claudeSession = loadSession(); |
| 59 | if (!claudeSession && process.env.LCHAT_INVITE) { |
| 60 | log("no session; provisioning from LCHAT_INVITE…"); |
| 61 | try { |
| 62 | claudeSession = await provision(process.env.LCHAT_INVITE); |
| 63 | log(`provisioned "${claudeSession.name}"`); |
| 64 | } catch (e) { |
| 65 | log(`provision failed: ${e?.message || e}`); |
| 66 | process.exit(1); |
| 67 | } |
| 68 | } |
| 69 | if (claudeSession) runBot({ name: AGENT_NAME, session: claudeSession }); |
| 70 | |
| 71 | // --- Eleven: the app guide + welcomer --------------------------------------- |
| 72 | let elevenSession = null; |
| 73 | try { |
| 74 | elevenSession = await loadElevenSession(); |
| 75 | } catch (e) { |
| 76 | log(`eleven provision failed: ${e?.message || e}`); |
| 77 | process.exit(1); |
| 78 | } |
| 79 | if (elevenSession) { |
| 80 | // Eleven runs a deterministic command floor (welcome + slash commands) that |
| 81 | // works with no API key, and "upgrades" to conversational AI when one is set — |
| 82 | // its badge (BOT vs AI) follows, self-declared on connect. See runtime.mjs. |
| 83 | runBot({ |
| 84 | name: ELEVEN_NAME, |
| 85 | session: elevenSession, |
| 86 | builtinPersona: ELEVEN_PERSONA, |
| 87 | builtinDocs: ELEVEN_DOCS, |
| 88 | tools: CHIP_TOOLS, |
| 89 | commands: true, |
| 90 | }); |
| 91 | // The welcomer is STOOD DOWN: the client-side welcome conversation |
| 92 | // (web/static/apps/welcome.js) greets every joiner now — one voice, not two |
| 93 | // Elevens saying hello. startWelcomer() in eleven.mjs stays for standalone |
| 94 | // runs; re-wire it here only if the client welcome ever retires. |
| 95 | } |
| 96 | |
| 97 | if (!claudeSession && !elevenSession) { |
| 98 | log("no chat identities — set LCHAT_INVITE and/or LCHAT_ELEVEN_INVITE (or provide sessions)"); |
| 99 | process.exit(1); |
| 100 | } |
| 101 | |