mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-06-09 12:53:16 +00:00
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:
@@ -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`.
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 -- "$@"
|
||||
|
||||
Reference in New Issue
Block a user