support-bot: implement postgresql (#6876)

* support-bot: sqlite/postgres backend via typed DbConfig and parseArgs flags

* support-bot: add README with setup and flags reference
This commit is contained in:
sh
2026-04-25 11:16:26 +00:00
committed by GitHub
parent d20af89a04
commit a836c50f60
6 changed files with 296 additions and 169 deletions
+88
View File
@@ -0,0 +1,88 @@
# SimpleX Support Bot
A business-address bot that triages incoming support chats, optionally runs them through Grok, and routes handoffs to a team group.
## Prerequisites
- Node.js (v18+; v24 tested)
- For PostgreSQL backend only: `libpq5` on the host (`apt install libpq5` on Debian/Ubuntu) and Linux x86_64
- `GROK_API_KEY` env var (xAI key) — only required when you want Grok replies; the bot runs without it
## 1. Build
```bash
cd apps/simplex-support-bot
./build.sh
```
This builds `@simplex-chat/types`, `simplex-chat` (native lib), and the bot. SQLite is the default.
To build with **PostgreSQL** instead, set the env var before running `./build.sh`:
```bash
SIMPLEX_BACKEND=postgres ./build.sh
```
Or put `simplex_backend=postgres` in `.npmrc` at the repo root (picked up by all nested `npm install`s).
## 2. Run — SQLite (default)
```bash
export GROK_API_KEY=xai-... # optional, only if you want Grok
./start.sh --team-group "Support Team"
```
Default locations:
- DB files: `./data/simplex_chat.db`, `./data/simplex_agent.db`
- State JSON: `./data/state.json`
Override any of those with flags — see [Flags](#flags).
## 3. Run — PostgreSQL
```bash
export GROK_API_KEY=xai-... # optional
./start.sh \
--team-group "Support Team" \
--pg-conn "postgres://simplex:pass@localhost/simplex_bot"
```
The bot must have been built with `SIMPLEX_BACKEND=postgres` (step 1). Schema prefix defaults to `simplex_v1` — pass `--pg-schema <name>` to namespace a different prefix.
## Flags
All flags are parsed via `node:util.parseArgs` (strict mode — unknown flags are rejected).
| Flag | Backend | Required | Default | Description |
|---|---|---|---|---|
| `--team-group` | | yes | — | team group display name |
| `--state-file` | both | no | `./data/state.json` | path to bot state JSON |
| `--sqlite-file-prefix` | sqlite | no | `./data/simplex` | DB file prefix (creates `<prefix>_chat.db`, `<prefix>_agent.db`) |
| `--sqlite-key` | sqlite | no | (unencrypted) | SQLCipher encryption key |
| `--pg-conn` | postgres | yes | — | PostgreSQL connection string |
| `--pg-schema` | postgres | no | `simplex_v1` | schema prefix used for bot tables |
| `-a` / `--auto-add-team-members` | | no | | comma-separated `ID:name` pairs (e.g. `1:Alice,2:Bob`) |
| `--timezone` | | no | `UTC` | IANA zone for weekend detection |
| `--complete-hours` | | no | `3` | auto-complete chats after N hours idle (`0` disables) |
| `--card-flush-seconds` | | no | `300` | debounce card state writes |
| `--context-file` | | required with `GROK_API_KEY` | | text file with Grok system context |
Flag values containing leading dashes must use `--flag=value` form (e.g. `--sqlite-key=-abc`).
## Environment variables
| Var | Purpose |
|---|---|
| `GROK_API_KEY` | xAI API key; enables Grok replies |
| `SIMPLEX_BACKEND` | install-time selector for the native lib backend (`sqlite` or `postgres`); `npm_config_simplex_backend` from `.npmrc` works the same way |
## State file
`--state-file` holds persisted IDs between runs (team group, Grok contact). It's a local JSON regardless of DB backend. Parent directory must exist.
## Troubleshooting
- **"Option '--X' argument is ambiguous"** — use `--X=value` form when the value starts with `-`.
- **Postgres: connect errors** — check that `libpq5` is installed and the connection string is correct. The native lib errors will surface at `ChatApi.init(...)` on startup.
- **Sqlite: DB files missing** — check the directory pointed to by `--sqlite-file-prefix` exists.
- **"--pg-conn is required when backend is postgres"** — you built with the postgres backend (see `installed.txt` in `packages/simplex-chat-nodejs/libs/`) but didn't pass `--pg-conn`.
+81 -2
View File
@@ -228,7 +228,8 @@ function makeCustomerMember(status = GroupMemberStatus.Connected) {
function makeConfig(overrides: Partial<any> = {}) {
return {
dbPrefix: "./test-data/simplex",
stateFile: "./test-data/state.json",
db: {type: "sqlite", filePrefix: "./test-data/simplex"},
teamGroup: {id: TEAM_GROUP_ID, name: "SupportTeam"},
teamMembers: [
{id: TEAM_MEMBER_1_ID, name: "Alice"},
@@ -2290,8 +2291,86 @@ describe("parseConfig Validation", () => {
.toThrow(/--complete-hours must be a non-negative integer, got "abc"/)
})
test("postgres backend without --pg-conn → throws", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "postgres"
try {
expect(() => parseConfig(baseArgs))
.toThrow(/--pg-conn is required when backend is postgres/)
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
})
test("postgres backend with --pg-conn → db is postgres DbConfig", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "postgres"
try {
const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://user:pass@localhost/db"])
expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://user:pass@localhost/db"})
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
})
test("postgres backend with --pg-schema → DbConfig carries schemaPrefix", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "postgres"
try {
const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://localhost/db", "--pg-schema", "bot"])
expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://localhost/db", schemaPrefix: "bot"})
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
})
test("sqlite backend (default) → db is sqlite DbConfig with default filePrefix", () => {
const prevBackend = process.env.SIMPLEX_BACKEND
const prevNpm = process.env.npm_config_simplex_backend
delete process.env.SIMPLEX_BACKEND
delete process.env.npm_config_simplex_backend
try {
const cfg = parseConfig(baseArgs)
expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex"})
} finally {
if (prevBackend !== undefined) process.env.SIMPLEX_BACKEND = prevBackend
if (prevNpm !== undefined) process.env.npm_config_simplex_backend = prevNpm
}
})
test("sqlite backend with --sqlite-key → DbConfig carries encryptionKey", () => {
const cfg = parseConfig([...baseArgs, "--sqlite-key", "secret"])
expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex", encryptionKey: "secret"})
})
test("unknown flag → parseArgs throws", () => {
expect(() => parseConfig([...baseArgs, "--team-gropu", "typo"]))
.toThrow()
})
test("missing --team-group → throws", () => {
expect(() => parseConfig([]))
.toThrow(/Missing required argument: --team-group/)
})
test("invalid SIMPLEX_BACKEND → throws", () => {
const prev = process.env.SIMPLEX_BACKEND
process.env.SIMPLEX_BACKEND = "mysql"
try {
expect(() => parseConfig(baseArgs))
.toThrow(/Invalid SIMPLEX_BACKEND: "mysql"/)
} finally {
if (prev === undefined) delete process.env.SIMPLEX_BACKEND
else process.env.SIMPLEX_BACKEND = prev
}
})
test("--complete-hours negative → throws", () => {
expect(() => parseConfig([...baseArgs, "--complete-hours", "-1"]))
// parseArgs refuses "-1" as a bare arg (ambiguous with a short flag), so use `=` form
expect(() => parseConfig([...baseArgs, "--complete-hours=-1"]))
.toThrow(/--complete-hours must be a non-negative integer, got "-1"/)
})
+39 -134
View File
@@ -19,6 +19,35 @@
"vitest": "^1.6.1"
}
},
"../../packages/simplex-chat-client/types/typescript": {
"name": "@simplex-chat/types",
"version": "0.5.0",
"license": "AGPL-3.0",
"dependencies": {
"typescript": "^5.9.2"
}
},
"../../packages/simplex-chat-nodejs": {
"name": "simplex-chat",
"version": "6.5.0-beta.10",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@simplex-chat/types": "^0.5.0",
"extract-zip": "^2.0.1",
"fast-deep-equal": "^3.1.3",
"node-addon-api": "^8.5.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.0.5",
"jest": "^30.2.0",
"ts-jest": "^29.4.6",
"typedoc": "^0.28.15",
"typedoc-plugin-markdown": "^4.9.0",
"typescript": "^5.9.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -731,12 +760,8 @@
]
},
"node_modules/@simplex-chat/types": {
"version": "0.3.0",
"resolved": "file:../../packages/simplex-chat-client/types/typescript",
"license": "AGPL-3.0",
"dependencies": {
"typescript": "^5.9.2"
}
"resolved": "../../packages/simplex-chat-client/types/typescript",
"link": true
},
"node_modules/@sinclair/typebox": {
"version": "0.27.10",
@@ -754,20 +779,11 @@
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
"devOptional": true,
"dev": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vitest/expect": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
@@ -890,14 +906,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"engines": {
"node": "*"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -961,6 +969,7 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
},
@@ -994,14 +1003,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -1084,38 +1085,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1139,20 +1108,6 @@
"node": "*"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -1259,7 +1214,8 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.11",
@@ -1279,14 +1235,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-addon-api": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
"integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/npm-run-path": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
@@ -1314,14 +1262,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/onetime": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
@@ -1376,11 +1316,6 @@
"node": "*"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1446,15 +1381,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -1545,16 +1471,8 @@
}
},
"node_modules/simplex-chat": {
"version": "6.5.0-beta.4.4",
"resolved": "file:../../packages/simplex-chat-nodejs",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
"@simplex-chat/types": "^0.3.0",
"extract-zip": "^2.0.1",
"fast-deep-equal": "^3.1.3",
"node-addon-api": "^8.5.0"
}
"resolved": "../../packages/simplex-chat-nodejs",
"link": true
},
"node_modules/source-map-js": {
"version": "1.2.1",
@@ -1643,6 +1561,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -1661,7 +1580,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true
"dev": true
},
"node_modules/vite": {
"version": "5.4.21",
@@ -1840,20 +1759,6 @@
"node": ">=8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/yocto-queue": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
+66 -22
View File
@@ -1,10 +1,16 @@
import {parseArgs} from "node:util"
import {api} from "simplex-chat"
export interface IdName {
id: number
name: string
}
export type Backend = "sqlite" | "postgres"
export interface Config {
dbPrefix: string
stateFile: string // local path to the bot's state JSON
db: api.DbConfig // passed to ChatApi.init / bot.run
teamGroup: IdName // name from CLI, id resolved at startup from state file
teamMembers: IdName[] // optional, empty if not provided
grokContactId: number | null // resolved at startup
@@ -15,6 +21,18 @@ export interface Config {
grokApiKey: string | null
}
// Mirrors packages/simplex-chat-nodejs/src/download-libs.js so runtime detection
// matches what was used at install time. Works whether the user installed via
// SIMPLEX_BACKEND env var, .npmrc (→ npm_config_simplex_backend), or the
// --simplex_backend=postgres CLI flag (also surfaced as npm_config_*).
export function detectBackend(): Backend {
const raw = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || "sqlite").toLowerCase()
if (raw !== "sqlite" && raw !== "postgres") {
throw new Error(`Invalid SIMPLEX_BACKEND: "${raw}". Must be "sqlite" or "postgres".`)
}
return raw
}
export function parseIdName(s: string): IdName {
const i = s.indexOf(":")
if (i < 1) throw new Error(`Invalid ID:name format: "${s}"`)
@@ -23,18 +41,6 @@ export function parseIdName(s: string): IdName {
return {id, name: s.slice(i + 1)}
}
function requiredArg(args: string[], flag: string): string {
const i = args.indexOf(flag)
if (i < 0 || i + 1 >= args.length) throw new Error(`Missing required argument: ${flag}`)
return args[i + 1]
}
function optionalArg(args: string[], flag: string, defaultValue: string): string {
const i = args.indexOf(flag)
if (i < 0 || i + 1 >= args.length) return defaultValue
return args[i + 1]
}
function parseNonNegativeInt(raw: string, flag: string): number {
const n = parseInt(raw, 10)
if (!Number.isFinite(n) || n < 0) {
@@ -44,35 +50,73 @@ function parseNonNegativeInt(raw: string, flag: string): number {
}
export function parseConfig(args: string[]): Config {
const {values} = parseArgs({
args,
strict: true,
options: {
"team-group": {type: "string"},
"state-file": {type: "string", default: "./data/state.json"},
"sqlite-file-prefix": {type: "string", default: "./data/simplex"},
"sqlite-key": {type: "string"},
"pg-conn": {type: "string"},
"pg-schema": {type: "string"},
"auto-add-team-members": {type: "string", short: "a"},
"timezone": {type: "string", default: "UTC"},
"complete-hours": {type: "string", default: "3"},
"card-flush-seconds": {type: "string", default: "300"},
"context-file": {type: "string"},
},
})
// Treat empty string as absent so `GROK_API_KEY=` behaves like unset
const grokApiKey = process.env.GROK_API_KEY || null
const dbPrefix = optionalArg(args, "--db-prefix", "./data/simplex")
const teamGroupName = requiredArg(args, "--team-group")
const backend = detectBackend()
let db: api.DbConfig
if (backend === "sqlite") {
// default guarantees non-undefined
const filePrefix = values["sqlite-file-prefix"]!
const encryptionKey = values["sqlite-key"]
db = encryptionKey
? {type: "sqlite", filePrefix, encryptionKey}
: {type: "sqlite", filePrefix}
} else {
const connectionString = values["pg-conn"]
if (!connectionString) {
throw new Error("--pg-conn is required when backend is postgres (PostgreSQL connection string)")
}
const schemaPrefix = values["pg-schema"]
db = schemaPrefix
? {type: "postgres", connectionString, schemaPrefix}
: {type: "postgres", connectionString}
}
const teamGroupName = values["team-group"]
if (!teamGroupName) throw new Error("Missing required argument: --team-group")
const teamGroup: IdName = {id: 0, name: teamGroupName}
const teamMembersRaw = optionalArg(args, "--auto-add-team-members", "") || optionalArg(args, "-a", "")
const teamMembersRaw = values["auto-add-team-members"] ?? ""
const teamMembers = teamMembersRaw
? teamMembersRaw.split(",").map(parseIdName)
: []
const timezone = optionalArg(args, "--timezone", "UTC")
const timezone = values["timezone"]!
try {
new Intl.DateTimeFormat("en-US", {timeZone: timezone, weekday: "short"})
} catch (err) {
throw new Error(`--timezone "${timezone}" is not a valid IANA time zone: ${(err as Error).message}`)
}
const completeHours = parseNonNegativeInt(optionalArg(args, "--complete-hours", "3"), "--complete-hours")
const cardFlushSeconds = parseNonNegativeInt(optionalArg(args, "--card-flush-seconds", "300"), "--card-flush-seconds")
const contextFileRaw = optionalArg(args, "--context-file", "")
const contextFile = contextFileRaw || null
const completeHours = parseNonNegativeInt(values["complete-hours"]!, "--complete-hours")
const cardFlushSeconds = parseNonNegativeInt(values["card-flush-seconds"]!, "--card-flush-seconds")
const contextFile = values["context-file"] || null
if (grokApiKey && !contextFile) {
throw new Error("GROK_API_KEY is set but --context-file is not provided. Grok requires a context file.")
}
return {
dbPrefix,
stateFile: values["state-file"]!,
db,
teamGroup,
teamMembers,
grokContactId: null,
+6 -4
View File
@@ -24,8 +24,10 @@ function writeState(path: string, state: BotState): void {
async function main(): Promise<void> {
const config = parseConfig(process.argv.slice(2))
// Do not log config.db.connectionString — typically contains credentials.
log("Config parsed", {
dbPrefix: config.dbPrefix,
stateFile: config.stateFile,
backend: config.db.type,
teamGroup: config.teamGroup,
teamMembers: config.teamMembers,
timezone: config.timezone,
@@ -34,7 +36,7 @@ async function main(): Promise<void> {
const grokEnabled = config.grokApiKey !== null
if (!grokEnabled) log("No GROK_API_KEY provided, disabling Grok support")
const stateFilePath = `${config.dbPrefix}_state.json`
const stateFilePath = config.stateFile
const state = readState(stateFilePath)
// Forward-reference for event handlers during init
@@ -47,7 +49,7 @@ async function main(): Promise<void> {
// userId persisted in state.json on first resolution — comparing by
// profile name is fragile to renames.
if (state.grokUserId !== undefined) {
const preChat = await api.ChatApi.init({type: "sqlite", filePrefix: config.dbPrefix})
const preChat = await api.ChatApi.init(config.db)
try {
const activeUser = await preChat.apiGetActiveUser()
if (activeUser && activeUser.userId === state.grokUserId) {
@@ -88,7 +90,7 @@ async function main(): Promise<void> {
log("Initializing main bot...")
const [chat, mainUser, mainAddress] = await bot.run({
profile: {displayName: "Ask SimpleX Team", fullName: "", image: supportImage},
dbOpts: {type: "sqlite", filePrefix: config.dbPrefix},
dbOpts: config.db,
options: {
addressSettings: {
businessAddress: true,
+16 -7
View File
@@ -5,15 +5,22 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# --- Required ---
# GROK_API_KEY xAI API key (env var)
# --team-group Team group display name
# GROK_API_KEY xAI API key (env var)
# --team-group Team group display name
# --pg-conn PostgreSQL connection string. Required only when
# simplex-chat-nodejs was installed with the postgres
# backend (SIMPLEX_BACKEND=postgres, .npmrc, or
# --simplex_backend=postgres at install time).
# --- Optional ---
# --db-prefix Database file prefix (default: ./data/simplex)
# --state-file Path to the bot's state JSON (default: ./data/state.json)
# --sqlite-file-prefix SQLite DB file prefix (default: ./data/simplex)
# --sqlite-key SQLCipher encryption key (default: unencrypted)
# --pg-schema PostgreSQL schema prefix (default: simplex_v1)
# --auto-add-team-members (-a) Comma-separated ID:name pairs (e.g. 1:Alice,2:Bob)
# --group-links Public group link(s) shown in welcome message
# --timezone IANA timezone for weekend detection (default: UTC)
# --complete-hours Hours of inactivity before auto-complete (default: 3)
# --group-links Public group link(s) shown in welcome message
# --timezone IANA timezone for weekend detection (default: UTC)
# --complete-hours Hours of inactivity before auto-complete (default: 3)
if [ -z "${GROK_API_KEY:-}" ]; then
echo "Error: GROK_API_KEY environment variable is required" >&2
@@ -25,4 +32,6 @@ if [ ! -f dist/index.js ]; then
exit 1
fi
exec node dist/index.js "$@"
# Run via `npm start` so npm exposes `.npmrc` values (e.g. simplex_backend=postgres)
# as `npm_config_*` env vars to the bot.
exec npm --silent start -- "$@"