Discord agent for Kabur Aja Dulu community.
Current direction:
- semi-autonomous community operations copilot
- event operations first
- analytics and reporting second
- content queue drafting third
- staff-facing knowledge support from curated Markdown docs only
The repository is set up to use repo-local OpenCode orchestration with:
buildfor reviewed implementation workresearchfor product, architecture, policy, and planning work
Start in docs/product-specs/kad-agent.md and docs/exec-plans/active/milestone-0-foundation-and-bot-skeleton.md.
- Use Node 24 (the supported engine; it includes
node:sqlite). - Install dependencies reproducibly:
npm ci - Keep
.envout of version control..env.exampledocuments key names only; inject runtime values through the process environment or a secret manager. - For a safe local smoke, set only
BOT_DRY_RUN=true(and optionallyDATABASE_PATH) or runnpm run start:dry-run. This initializes the local SQLite schema, seeds foundation data, and runs one reminder sweep without Discord credentials or network calls. - Before live Discord validation, set
BOT_DRY_RUN=falseand inject the required logical keysDISCORD_APP_ID,DISCORD_BOT_TOKEN, andDISCORD_ALLOWED_GUILD_IDS. The guild allowlist is a comma-separated list of Discord snowflakes.npm run startandnpm run register-commandsrefuse live use without these controls. - Publication observe/active mode additionally requires one target guild. Set either
DISCORD_TARGET_GUILD_IDorDISCORD_TARGET_GUILD_ID_FILE, never both. A file-backed target may reuseDISCORD_ALLOWED_GUILD_IDS_FILEonly when that protected file contains exactly one guild ID;DISCORD_TARGET_GUILD_NAMEremains non-secret configuration. - Initialize SQLite:
npm run db:init - Register slash commands in the target guild explicitly:
npm run register-commands -- --guild-id <discord-guild-snowflake> - Start the bot runtime:
npm run start - In Discord, as a guild admin, run
/setup e1-configureand set the announcement channel, host voice channel, timezone, and at least one staff role. - Optionally confirm with
/setup e1-show. - Then run
/event create-language-clubwith adate(YYYY-MM-DD) andtime(HH:mm).
Success looks like this:
- Discord: the command returns an ephemeral success reply like
Event berhasil dibuat dan dipublish..., and the configured announcement channel receives the Language Club announcement message. - SQLite:
language_club_guild_configandlanguage_club_staff_rolescontain the guild setup, and the latesteventsrow isstate=publishedwith the configured announcement/voice channel IDs and a non-emptydiscord_announcement_message_id;event_state_transitionsrecordsdraftedthenpublishedfor the sameevent_id.
The production build emits compiled JavaScript and declarations under dist/:
npm run typecheck
npm test
npm run build
npm run audit
npm run start:dry-runThe CI workflow runs those checks and a container build with Node 24 and npm ci, using only the contents: read permission and no repository secrets.
Set the repository variable KAD_INFRA_MIGRATION=true to pause both scheduled
and manual staging publication. The workflow reports the pause in its job
summary without loading publication secrets or contacting Discord. Already
running jobs must be drained or canceled separately. Cloudflare continues
serving its last accepted snapshot with its existing freshness status.
Clear the migration variable only after the target environment is verified and
one writer is selected. KADDY_RUNTIME_PUBLICATION_ACTIVE is the separate
permanent writer-handoff guard; follow the
single-writer handoff
when switching to the restored runtime. A migration pause does not establish
that the runtime is active.
The multi-stage image compiles the app from lockfile-resolved dependencies on
the supported Node 24 major and runs it as the unprivileged numeric identity
10001:10001, matching the homelab volume and secret ownership contract.
/data is a persistent volume, and the compiled entrypoint is
/nodejs/bin/node dist/index.js. The final distroless runtime base is pinned by
digest and contains no npm, shell, or package manager. The two build-only
stages intentionally track the supported node:24-bookworm-slim major and do
not ship in the release image; rotating either builder tag or runtime digest is
an explicit release-maintenance task.
docker build -t <image-name>:<tag> .
docker volume create <volume-name>
docker run --rm --init \
--env BOT_DRY_RUN=true \
--mount type=volume,src=<volume-name>,dst=/data \
<image-name>:<tag> --dry-runFor a live runtime, inject BOT_DRY_RUN=false, DISCORD_APP_ID=<discord-app-id>, DISCORD_BOT_TOKEN=<discord-bot-token>, and DISCORD_ALLOWED_GUILD_IDS=<discord-guild-id> at runtime and keep the same /data volume. Replace placeholders before executing; do not put secret values in this README, the image, or a committed .env file.
docker run --rm --init \
--env BOT_DRY_RUN=false \
--env DISCORD_APP_ID=<discord-app-id> \
--env DISCORD_BOT_TOKEN=<discord-bot-token> \
--env DISCORD_ALLOWED_GUILD_IDS=<discord-guild-id> \
--mount type=volume,src=<volume-name>,dst=/data \
<image-name>:<tag>The container health check calls /readyz over loopback. It turns healthy only
after migrations, seeds, and the Discord login have completed. /healthz
reports process liveness but does not assert Discord readiness.
Do not dump the SQLite database into CI logs or support tickets. Use the automated repository tests for state-transition checks and a scoped, access-controlled operator procedure for any production inspection.