Imported from nervous-rob/goobster (
AGENTS.md). Install upstream withnpx skills add nervous-rob/goobster. Copyright stays with the author.
AGENTS.md
Cursor Cloud specific instructions
Goobster is a self-hostable Node.js Discord bot using discord.js, a local SQLite database
(better-sqlite3), system FFmpeg, and pluggable AI providers (OpenAI / Anthropic / Gemini /
local Ollama). All cloud integrations are optional and degrade gracefully.
Repo layout. This is an npm-workspaces monorepo (packages/*, apps/*). Shared code —
services, the chat pipeline, the database facade, config resolution, the Discord gateway seam,
and the portal backend — lives in packages/core (@goobster/core). The apps are apps/bot
(the Discord client; entry apps/bot/index.js), apps/api (the split deployment's web
backend), apps/sandbox (the code-execution runner), and apps/web (the React portal client,
the only TypeScript in the repo). The default lite deployment is still a single process:
apps/bot serves the portal in-process against SQLite. Apps import core; core must never
import an app, and that boundary is ESLint-enforced.
Standard commands live in package.json and README.md; prefer those. Key ones:
- Dev run:
npm run dev(nodemonapps/bot/index.js) — does NOT calldeploy-commands. - Prod-style run:
npm start(runsapps/bot/deploy-commands.jsthenapps/bot/index.js). - DB init:
npm run db-init(createsdata/goobster.sqlite;packages/core/db/schema.sqlis also applied automatically on every DB open). - Tests:
npm test(full unit suite, no keys) /npm run test:live(optional provider checks; skips when env vars are unset). Lint:npm run lint. Portal browser journeys:npm run build:web && npm run test:e2e(Playwright + Chromium; first time runnpm run test:e2e:install).
Non-obvious caveats (discovered during setup)
-
config.jsonis required and gitignored. It stays at the repo root (the bot resolves it throughpackages/core/runtimePaths.js).apps/bot/index.jsandapps/bot/deploy-commands.jsread Discord credentials (token,clientId,guildIds) fromconfig.jsononly — NOT from env vars. The VM starts without it, so create it before running the bot. AI/integration keys (OPENAI_API_KEY,ANTHROPIC_API_KEY,GEMINI_API_KEY,PERPLEXITY_API_KEY,ELEVENLABS_API_KEY) ARE read from env bypackages/core/config/aiConfig.js/packages/core/config.js, so those can come from injected secrets. Buildconfig.jsonfrom secrets before starting (guild id may be a bare id or a JSON array; snowflakes must be quoted strings):if [ ! -f config.json ]; then case "$DISCORD_GUILD_IDS" in \[*) GID_JSON="$DISCORD_GUILD_IDS" ;; # already a JSON array *) GID_JSON="[\"$DISCORD_GUILD_IDS\"]" ;; esac cat > config.json <<JSON { "clientId": "${DISCORD_CLIENT_ID}", "guildIds": ${GID_JSON}, "token": "${DISCORD_BOT_TOKEN}", "DEFAULT_PROMPT": "You are Goobster, a quirky and clever Discord bot.", "ai": { "provider": "" } } JSON fi(
ai.providerempty = auto-detect: OpenAI ifOPENAI_API_KEYset, else Anthropic, else Gemini, else Ollama.) Thennpm run deploy-commandsregisters slash commands to the guild, andnpm run dev(ornode apps/bot/index.js) starts the bot. A successful connect logsReady! Logged in as <tag>. -
Lint, smoke, typecheck, build, and tests all pass and are enforced in CI (
.github/workflows/ci.yml), across two engine jobs. Thetest (sqlite)job runsnpm run lint(ESLint flat config ineslint.config.js, zero errors required),npm run smoke(every module mustrequire()cleanly with a minimal config),npm run typecheck:web,npm run build:web, and the named Jest groups intests/ciGroups.js. Thetest (postgres)job re-runs those groups against a pgvector container withGOOBSTER_DB_URLset, so a change has to pass on both database engines. A newtests/*.test.jsfile must be added to exactly one group or the inventory step fails. Optional live provider tests (npm run test:live) run on trustedmainpushes andworkflow_dispatchonly; they are not part ofboth engines. -
Local Postgres 17 + pgvector is available for the engine-parity suite.
scripts/ensure-local-postgres.shis idempotent (install packages, start the cluster even when systemd is offline, create role/dbgoobster/goobster, enablevector+citext).npm teststays on throwaway SQLite; do not exportGOOBSTER_DB_URLglobally. After the script reports ready:npm run test:postgres # or a single file: GOOBSTER_DB_URL=postgres://goobster:goobster@127.0.0.1:5432/goobster \ GOOBSTER_PG_TEST_ISOLATE=1 npx jest tests/projectTriggerService.test.jsIsolated PG schemas apply the full
schema.sqlon a suite's first query, sotests/setup/perSuite.jsraises the Jest timeout to 20s whenGOOBSTER_DB_URLis set. Never binddatetime('now', @param)— the dialect only rewrites literal modifiers; compute UTCYYYY-MM-DD HH:MM:SSin JS and bind the text. -
npm testruns the Jest specs intests/*.test.js(e.g.privacyService.test.js,memoryVecIndex.test.js) and must pass. They use a throwaway SQLite file viaGOOBSTER_DB_PATH, so no config or network is needed. The othertests/test*.jsfiles are standalone manual scripts, not Jest specs. Playwright lives ine2e/*.spec.jsand is not part ofnpm test. Live provider checks live intests/live/*.live.test.jsand skip when the matching API key is unset (npm run test:live). Each unit spec belongs to one CI group intests/ciGroups.js. -
Portal Playwright journeys need Chromium and a built React client.
e2e/server.jsmountscreateWebAppApp(createWebAppContext({ gateway, config: { webapp: { enabled: true, devMode: true } } }))against a throwaway SQLite file — no Discord token. Runnpm run build:webthennpm run test:e2e. First time on a machine:npm run test:e2e:install(npx playwright install --with-deps chromium). CI'stest (playwright)job does the same. Theboth enginesaggregator does not wait on this job. -
Memory recall uses the sqlite-vec extension (loaded in
packages/core/db/index.js, prebuilts for x64 and ARM64) with per-dimensionmemory_vec_<dims>virtual tables, falling back to a brute-force scan when the extension can't load. If you add a deletion path formemory_embeddings, callmemoryService.cleanupVecIndex()afterwards so vectors don't outlive their memories. -
The attention system is opt-in per person and needs no Discord token to exercise.
documentation/attention.mdis the spec. Nothing runs until somebody has anattention_policiesrow (/attention enable), so a fresh database is inert by design — if a sweep seems to do nothing, check enrollment first. Everything except delivery works headless:attentionService.sweepUser({ policy, gateway })takes any object withsendDm, andpersonalHeartbeatServiceaccepts a fake client, so the whole pipeline (generators, scoring, triage, notices, calibration) can be driven from a plain Node script against a throwaway SQLite file. Watches fire offdomainEventBus, soobservatoryService._finishJob(jobId, 'COMPLETED')is enough to exercise a real condition end to end — but a fake DM channel needsmessages: { fetch: async () => [] }as well assend, because the chat pipeline reads recent history through it. To browser-test the Noticed pane without a bot token, mountcreateWebAppApp(createWebAppContext({ gateway, config: { webapp: { enabled: true, devMode: true } } }))— both exported frompackages/core/web/appApi.js— on a plain express app; it serves the built React client fromapps/web/dist(runnpm run build:webfirst). Note the score bands inpackages/core/config/attentionConfig.jsare calibrated to the rangeU × I × C × A − Kcan actually reach (~0.12/0.28/0.45/0.75) — respacing them across[0, 1]silently makes every band aboveinboxunreachable. -
Music downloads (
/spotdl,/play url:) needspotdlandyt-dlp. The Cloud environment install puts them in~/.local/goobster-venv(same path asscripts/install-rpi.sh/scripts/ensure-music-cli.sh). The bot auto-discovers that venv plus~/.local/binand/opt/venv. The snapshot this VM booted from did not include those CLIs, andpython3-venvis not on the base image — installpython3.12-venvfirst orpython3 -m venvfails withNo module named ensurepip. ACLI not founderror that lists every candidate as missing means the environment snapshot is stale; Save a new environment after the music-CLI install lands. -
The sandbox Python toolkit needs two apt packages in this VM.
npm run sandbox-pythonbuilds a venv atdata/sandbox/venv(gitignored) from the catalog inpackages/core/config/sandboxPackages.js— core numerics/plotting plus theastroandimagingbundles, ~700 MB, ~30 s from a warm network. The VM ships withoutensurepip, so the venv creation fails untilsudo apt install -y python3.12-venv; addsudo apt install -y bubblewrapto exercise the strongest isolation rung (otherwise runs fall back tounshare -rn). Both installs are quick. A sandbox run can be driven straight from Node without Discord: enablerequire('@goobster/core/config/sandboxConfig').enabled, thensandboxService.run({ language: 'python', code, userId, projectDir }). -
Spitball Expeditions run end to end in this VM. The autonomous research subsystem (
documentation/spitball_expeditions.md; servicesspitballExpedition*, pipelinespitballResearchPipeline) is exercisable headless: create an expedition withspitballExpeditionService.createExpeditionand drive it withspitballExpeditionRunner.kick(id)against a throwaway SQLite file — the pipeline needsOPENAI_API_KEY(or another provider key) for real research, and works with just Wikipedia whenPERPLEXITY_API_KEYis absent. Tests inject fake pipelines/providers (seetests/spitballResearchPipeline.test.js), sonpm testneeds no keys or network. In the portal it is Spitball → Expeditions (a focused live run completes in about a minute with real keys). -
Local Ollama inference (
ollama serve) segfaults in this VM (llama-server ... segmentation fault), across multiple small models and with flash-attention disabled. The AI routing layer (packages/core/services/aiService.js→packages/core/services/ollamaService.js) works, but local generation does not complete here. For an end-to-end chat demo, use a cloud provider key (OPENAI_API_KEYorGEMINI_API_KEY) rather than the local Ollama fallback. -
The bot exposes an Express health endpoint at
http://localhost:3000/health(served byapps/bot/web/server.js). On invalid Discord token,apps/bot/index.jslogs in, fails withTokenInvalid, and callsprocess.exit(1)— a real bot token is required to stay connected. -
Web app browser testing: set
"webapp": { "enabled": true, "devMode": true }inconfig.jsonand openhttp://localhost:3000/app/— dev mode mints a session for any snowflake-shaped user id without OAuth (real Discord OAuth needs a publicwebapp.publicUrl+ portal redirect, unavailable in this VM). Guild-scope dashboard routes verify real guild membership through the bot client, so use an id that is actually a member of the connected guild (e.g. the guild owner) to exercise guild scopes and the knowledge-graph view (Manage Server gated).