Initial commit: Discord-Claude Gateway with event-driven agent runtime

This commit is contained in:
2026-02-22 13:34:26 -05:00
parent 82b0905a98
commit 68f24d50e1
5 changed files with 602 additions and 338 deletions

150
docs/FUTURE-FEATURES.md Normal file
View File

@@ -0,0 +1,150 @@
# Aetheel — Future Features Roadmap
Features inspired by nanoclaw that can be implemented in the CLI-based gateway. Excludes Docker containers, WhatsApp channel, and Agent SDK (already ruled out).
## Quick Wins (hours)
### 1. Message History Storage
Store inbound/outbound messages in a JSON file per channel. Nanoclaw uses SQLite, but a simple `config/messages/{channelId}.json` would work. This lets the agent see conversation history even after session expiry.
How nanoclaw does it: SQLite table with id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message columns. Indexed by timestamp.
Our approach: JSON file per channel at `config/messages/{channelId}.json`. Append-only. Include sender name, content, timestamp, and direction (inbound/outbound). Load recent messages into the system prompt or pass as context.
### 2. Structured Logging
Replace `console.log` with a proper logger (pino). Nanoclaw logs everything with structured context (group, duration, event type). Makes debugging way easier on a headless server.
How nanoclaw does it: Pino logger with pretty-printing, configurable log levels via LOG_LEVEL env var, uncaught exception routing, container logs saved to disk.
Our approach: Install pino, replace all console.log/error/warn calls, add log levels, save logs to `config/logs/`. Structured JSON logs make it easy to grep and filter.
### 3. Conversation Archiving
Before sessions get too long, archive transcripts to `config/conversations/`. Nanoclaw does this via a PreCompact hook that saves the full transcript as markdown before Claude compacts its context.
Our approach: After each CLI response, optionally append the exchange (prompt + response) to `config/conversations/{channelId}/{date}.md`. This creates a human-readable conversation log separate from Claude's internal session storage.
### 4. Retry with Backoff
When the Claude CLI fails, retry with exponential backoff (5s, 10s, 20s, 40s, 80s) up to 5 times. Nanoclaw does this for container failures.
How nanoclaw does it: GroupQueue tracks retryCount per group, uses `BASE_RETRY_MS * 2^retryCount` delay, max 5 retries, resets on success.
Our approach: Wrap the `executeClaude` call in a retry loop. On transient errors (timeout, CLI crash), wait and retry. On permanent errors (auth failure, invalid session), fail immediately.
## Medium Effort (day or two)
### 5. Agent-Managed Tasks via File-Based IPC
The big one from nanoclaw. The agent can create, pause, resume, and cancel scheduled tasks dynamically during a conversation.
How nanoclaw does it: Custom MCP server with tools like `schedule_task`, `list_tasks`, `pause_task`, `resume_task`, `cancel_task`. Tasks stored in SQLite with schedule_type (cron/interval/once), context_mode (group/isolated), and full lifecycle tracking.
Our approach (simpler): The agent writes task definitions to `config/tasks.json` using the Write tool. The gateway polls this file periodically and schedules/unschedules tasks accordingly. Task format:
```json
[
{
"id": "morning-briefing",
"prompt": "Good morning! Check email and summarize.",
"schedule_type": "cron",
"schedule_value": "0 9 * * *",
"status": "active",
"target_channel": "1475008084022788312"
}
]
```
The agent can add/remove/modify entries. The gateway watches the file and updates schedulers.
### 6. Multi-Turn Message Batching
Instead of sending one message at a time to Claude, batch multiple messages that arrive while the agent is processing.
How nanoclaw does it: Collects all messages since the last agent run timestamp, formats them as XML (`<messages><message sender="..." time="...">content</message></messages>`), sends the batch as a single prompt.
Our approach: When a message arrives while the agent is already processing for that channel, queue it. When the current processing finishes, batch all queued messages into a single prompt with sender/timestamp metadata. This prevents the agent from missing rapid-fire messages.
### 7. Idle Timeout for Sessions
Auto-clear sessions after a configurable period of inactivity per channel.
How nanoclaw does it: 30-minute idle timeout per container. After last output, a timer starts. If no new messages arrive, the container is closed and the session ends.
Our approach: Track `lastActivityTimestamp` per channel in the session manager. Run a periodic check (every 5 minutes) that removes sessions older than the timeout (default 30 min). This prevents stale sessions from accumulating and keeps memory.md relevant.
### 8. Per-Channel Isolation
Different channels could have different persona configs. A "work" channel gets a professional persona, a "fun" channel gets a casual one.
How nanoclaw does it: Per-group folders with separate CLAUDE.md, sessions, memory, and container mounts. Each group is fully isolated.
Our approach: Support a `config/channels/{channelId}/` override directory. If it exists, load persona files from there instead of the root config. Falls back to root config for missing files. This lets you customize identity.md or soul.md per channel.
## Bigger Features (multi-day)
### 9. IPC-Based Proactive Messaging
Let the agent send messages to Discord channels proactively — not just as a response to a user message.
How nanoclaw does it: Filesystem-based IPC. The agent writes a JSON file to `/workspace/ipc/messages/` with `{type: "message", chatJid: "...", text: "..."}`. The host polls the directory every second and sends the message.
Our approach: Create a `config/ipc/outbound/` directory. The agent writes JSON files there using the Write tool. The gateway polls every 2 seconds, reads new files, sends messages to the specified Discord channel, and deletes the processed files. This enables:
- Agent sending follow-up messages after thinking
- Agent notifying you about something it found during a heartbeat
- Agent sending messages to channels it's not currently chatting in
File format:
```json
{
"type": "message",
"channelId": "1475008084022788312",
"text": "Hey, I found something interesting in your email!"
}
```
### 10. Skills Engine (Simplified)
A system for adding capabilities via markdown skill files that get loaded into the system prompt.
How nanoclaw does it: Full manifest-based system with YAML manifests, dependency resolution, conflict detection, file operations (rename, delete, move), backup/restore, rebase capability, and resolution caching.
Our approach (much simpler): A `config/skills/` directory where each subdirectory contains a `SKILL.md` file. Skills are loaded into the system prompt as additional sections. The agent can reference skills by name. No dependency management — just markdown files that add context.
```
config/skills/
├── web-research/
│ └── SKILL.md → "When asked to research, use WebSearch and WebFetch..."
├── code-review/
│ └── SKILL.md → "When reviewing code, focus on security, performance..."
└── email-helper/
└── SKILL.md → "When checking email, use the Gmail API via..."
```
### 11. SQLite Storage
Replace JSON files with SQLite for messages, sessions, tasks, and state. Better for concurrent access, querying, and doesn't corrupt on partial writes.
How nanoclaw does it: Single SQLite database at `store/messages.db` with tables for chats, messages, scheduled_tasks, task_run_logs, router_state, sessions, registered_groups. Uses better-sqlite3 (synchronous API).
Our approach: Install better-sqlite3, create a single `config/aetheel.db` with tables for sessions, messages, tasks, and state. Migrate existing JSON files on first run.
### 12. Secrets Management
Prevent API keys and sensitive data from leaking through agent tool use.
How nanoclaw does it: Secrets passed via stdin (never written to disk), stripped from Bash subprocess environments via a PreToolUse hook, never mounted as files in containers.
Our approach: Since we use `--dangerously-skip-permissions`, the agent can run Bash commands that might echo environment variables. Add a sanitization step: before passing the prompt to Claude, strip any env var values that look like secrets from the system prompt. Also consider using `--disallowedTools Bash` if the agent doesn't need shell access.
## Priority Recommendation
1. Structured logging (immediate debugging value)
2. Message history storage (conversation context)
3. Retry with backoff (reliability)
4. IPC-based proactive messaging (agent autonomy)
5. Agent-managed tasks (dynamic scheduling)
6. Conversation archiving (audit trail)
7. Everything else as needed

437
docs/PROCESS-FLOW.md Normal file
View File

@@ -0,0 +1,437 @@
# Aetheel — Process Flow
How a Discord message becomes an AI response, step by step.
## The Big Picture
```
Discord User Aetheel Gateway Claude Code CLI
│ │ │
│ @Aetheel what's 2+2? │ │
├──────────────────────────────► │ │
│ │ 1. Extract prompt "what's 2+2?" │
│ │ 2. Check concurrency limit │
│ │ 3. Enqueue message event │
│ │ 4. Read config/*.md files │
│ │ 5. Assemble system prompt │
│ │ 6. Write prompt to temp file │
│ │ 7. Spawn CLI process │
│ │ │
│ │ claude -p "what's 2+2?" │
│ │ --output-format json │
│ │ --append-system-prompt-file ... │
│ │ --dangerously-skip-permissions │
│ ├──────────────────────────────────► │
│ │ │
│ │ ◄── JSON stream (init, result) │
│ │ ◄─────────────────────────────────┤
│ │ │
│ │ 8. Parse session_id from init │
│ │ 9. Parse result text │
│ │ 10. Split if > 2000 chars │
│ "2 + 2 = 4" │ │
│ ◄──────────────────────────────┤ │
│ │ 11. Save session for channel │
```
## Step-by-Step: Discord Message → Response
### Step 1: Message Arrives in Discord
A user types `@Aetheel what's the weather like?` in a Discord channel.
Discord delivers this to the bot as a `messageCreate` event via the WebSocket gateway.
**File:** `src/discord-bot.ts``setupMessageHandler()`
```
Raw message content: "<@1473096872372600978> what's the weather like?"
Author: tanmay11k6417 (bot: false)
Channel: 1475008084022788312
```
### Step 2: Message Filtering & Prompt Extraction
The bot checks:
1. Is the author a bot? → Skip (prevents feedback loops)
2. Does the message mention the bot? → Continue
3. Extract the prompt by stripping all mention tags
**File:** `src/discord-bot.ts``extractPromptFromMention()`
```
Input: "<@1473096872372600978> what's the weather like?"
Output: "what's the weather like?"
```
The regex `/<@[!&]?\d+>/g` strips user mentions (`<@ID>`), nickname mentions (`<@!ID>`), and role mentions (`<@&ID>`).
### Step 3: Prompt Handler (Gateway Core)
The extracted prompt is wrapped in a `Prompt` object and passed to the gateway core's `onPrompt` handler.
**File:** `src/gateway-core.ts``onPrompt` callback
```typescript
{
text: "what's the weather like?",
channelId: "1475008084022788312",
userId: "123456789",
guildId: "987654321"
}
```
The handler checks:
- Is the gateway shutting down? → Reply "Gateway is shutting down"
- Is `activeQueryCount >= maxConcurrentQueries` (default 5)? → Reply "System is busy"
- Otherwise: increment counter, send typing indicator, enqueue event
### Step 4: Event Queue
The prompt becomes a **message event** in the unified event queue.
**File:** `src/event-queue.ts`
```typescript
{
id: 2, // Monotonically increasing
type: "message",
payload: {
prompt: {
text: "what's the weather like?",
channelId: "1475008084022788312",
userId: "123456789",
guildId: "987654321"
}
},
timestamp: "2026-02-22T10:30:00.000Z",
source: "discord"
}
```
The queue processes events one at a time (FIFO). If a heartbeat or cron event is ahead in the queue, the message waits.
### Step 5: Agent Runtime — Read Config Files
When the event reaches the front of the queue, the Agent Runtime reads ALL markdown config files fresh from disk.
**File:** `src/markdown-config-loader.ts``loadAll()`
```
config/
├── identity.md → Agent name, role, vibe
├── soul.md → Personality, tone, values
├── agents.md → Operating rules, safety boundaries
├── user.md → Info about the human
├── memory.md → Long-term memory (agent can write to this)
└── tools.md → Tool configs, API notes
```
Files are read fresh every time — edit them while the gateway is running and the next event picks up changes.
If `memory.md` doesn't exist, it's auto-created with `# Memory\n`.
### Step 6: Assemble System Prompt
The markdown file contents are concatenated into a single system prompt with section headers.
**File:** `src/system-prompt-assembler.ts``assemble()`
The assembled prompt looks like this:
```
You may update your long-term memory by writing to memory.md using the Write tool.
Use this to persist important facts, lessons learned, and context across sessions.
## Identity
# Identity
- **Name:** Aetheel
- **Vibe:** Helpful, sharp, slightly witty
- **Emoji:** ⚡
## Personality
# Soul
Be genuinely helpful. Have opinions. Be resourceful before asking.
Keep responses concise for Discord. Use markdown formatting.
## Operating Rules
# Operating Rules
Be helpful and concise. Keep Discord messages short.
## Cron Jobs
...
## User Context
# User Context
- **Name:** Tanmay
- **Timezone:** IST
...
## Long-Term Memory
# Memory
- Tanmay prefers short responses
- Project aetheel-2 is the Discord gateway
...
## Tool Configuration
# Tool Configuration
(empty or tool-specific notes)
```
Sections with null or empty content are omitted entirely.
### Step 7: Write System Prompt to Temp File
The assembled system prompt is written to a temporary file because it can be thousands of characters — too large for a CLI argument.
**File:** `src/agent-runtime.ts``executeClaude()`
```
/tmp/aetheel-prompt-1d6c77f1-4a4e-49f8-ae9b-cff6fb47b971.txt
```
This file is deleted after the CLI process completes.
### Step 8: Spawn Claude CLI
The gateway spawns the Claude Code CLI as a child process.
**File:** `src/agent-runtime.ts``runClaude()`
The actual command:
```bash
claude \
-p "what's the weather like?" \
--output-format json \
--dangerously-skip-permissions \
--append-system-prompt-file /tmp/aetheel-prompt-xxx.txt \
--allowedTools Read \
--allowedTools Write \
--allowedTools Edit \
--allowedTools Glob \
--allowedTools Grep \
--allowedTools WebSearch \
--allowedTools WebFetch \
--max-turns 25
```
Key flags:
- `-p` — Print mode (non-interactive, exits after response)
- `--output-format json` — Returns JSON array of message objects
- `--dangerously-skip-permissions` — No interactive permission prompts
- `--append-system-prompt-file` — Appends our persona/memory to Claude's default prompt
- `--allowedTools` — Which tools Claude can use (one flag per tool)
- `--max-turns` — Prevents runaway agent loops
- `--resume SESSION_ID` — Added when resuming an existing conversation
The process runs with `cwd` set to the `config/` directory, so Claude can read/write files there (like `memory.md`).
`stdin` is set to `"ignore"` to prevent the CLI from waiting for interactive input.
### Step 9: Session Resumption
If this channel has chatted before, the session manager has a stored session ID.
**File:** `src/session-manager.ts`
```
config/sessions.json:
{
"1475008084022788312": "37336c32-73cb-4cf5-9771-1c8f694398ff"
}
```
When a session ID exists, `--resume 37336c32-73cb-4cf5-9771-1c8f694398ff` is added to the CLI args. Claude loads the full conversation history from `~/.claude/` and continues the conversation.
### Step 10: Parse CLI Output (Streaming)
The CLI returns a JSON array on stdout. The gateway parses it as chunks arrive.
**File:** `src/agent-runtime.ts``runClaude()` stdout handler
Example CLI output:
```json
[
{
"type": "system",
"subtype": "init",
"session_id": "37336c32-73cb-4cf5-9771-1c8f694398ff",
"tools": ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebSearch", "WebFetch"]
},
{
"type": "assistant",
"message": { "content": [{ "type": "text", "text": "Let me check..." }] }
},
{
"type": "result",
"subtype": "success",
"result": "I don't have access to real-time weather data, but I can help you check! Try asking me to search the web for current weather in your area.",
"session_id": "37336c32-73cb-4cf5-9771-1c8f694398ff",
"is_error": false,
"cost_usd": 0.003
}
]
```
The parser extracts:
1. `session_id` from the `init` message → stored for future resumption
2. `result` from the `result` message → sent to Discord
When streaming is active, result text is sent to Discord immediately as it's parsed, before the CLI process exits.
### Step 11: Response Formatting & Delivery
The result text is split into Discord-safe chunks (max 2000 characters each).
**File:** `src/response-formatter.ts``splitMessage()`
If the response contains code blocks that span a split boundary, the formatter closes the code block with ` ``` ` at the end of one chunk and reopens it with ` ``` ` at the start of the next.
The chunks are sent sequentially to the Discord channel via the bot.
### Step 12: Session Persistence
The session ID is saved to `config/sessions.json` so it survives gateway restarts.
**File:** `src/session-manager.ts``saveToDisk()`
Next time the user sends a message in the same channel, the conversation continues from where it left off.
---
## Other Event Types
### Heartbeat Flow
```
Timer fires (every N seconds)
→ HeartbeatScheduler creates heartbeat event
→ Event enters queue
→ AgentRuntime reads config files, assembles prompt
→ CLI runs with heartbeat instruction as prompt
→ Response sent to OUTPUT_CHANNEL_ID
```
Example heartbeat.md:
```markdown
## check-email
Interval: 1800
Instruction: Check my inbox for anything urgent. If nothing, reply HEARTBEAT_OK.
```
The instruction becomes the `-p` argument to the CLI.
### Cron Job Flow
```
Cron expression matches (e.g., "0 9 * * *" = 9am daily)
→ CronScheduler creates cron event
→ Event enters queue
→ AgentRuntime reads config files, assembles prompt
→ CLI runs with cron instruction as prompt
→ Response sent to OUTPUT_CHANNEL_ID
```
Cron jobs are defined in `config/agents.md`:
```markdown
## Cron Jobs
### morning-briefing
Cron: 0 9 * * *
Instruction: Good morning! Check email and give me a brief summary.
```
### Hook Flow
```
Lifecycle event occurs (startup, shutdown)
→ HookManager creates hook event
→ Event enters queue
→ AgentRuntime reads config files, assembles prompt
→ CLI runs with hook instruction as prompt
→ Response sent to OUTPUT_CHANNEL_ID
```
Hooks are defined in `config/agents.md`:
```markdown
## Hooks
### startup
Instruction: Say hello, you just came online.
### shutdown
Instruction: Save important context to memory.md before shutting down.
```
`agent_begin` and `agent_stop` hooks fire inline (not through the queue) before and after every non-hook event.
---
## What Gets Sent to Claude
For every event, Claude receives:
1. **Default Claude Code system prompt** (built-in, from the CLI)
2. **Appended system prompt** (from our assembled markdown files):
- Identity (who the agent is)
- Personality (how it behaves)
- Operating rules (safety, workflows)
- User context (who it's helping)
- Long-term memory (persistent facts)
- Tool configuration (API notes)
- Preamble about writing to memory.md
3. **The prompt text** (user message, heartbeat instruction, or cron instruction)
4. **Session history** (if resuming via `--resume`)
5. **Allowed tools** (Read, Write, Edit, Glob, Grep, WebSearch, WebFetch)
Claude runs in the `config/` directory, so it can read and write files there — including updating `memory.md` with new facts.
---
## File Map
```
src/
├── index.ts ← Entry point: creates GatewayCore, registers shutdown handler
├── gateway-core.ts ← Orchestrator: wires everything, manages lifecycle
├── config.ts ← Reads env vars (DISCORD_BOT_TOKEN, etc.)
├── discord-bot.ts ← Discord.js wrapper: messages, slash commands, typing
├── event-queue.ts ← FIFO queue: all events (message, heartbeat, cron, hook)
├── agent-runtime.ts ← Core engine: reads configs, spawns CLI, parses output
├── markdown-config-loader.ts ← Reads config/*.md files fresh each event
├── system-prompt-assembler.ts ← Concatenates markdown into system prompt with headers
├── session-manager.ts ← Channel → session ID mapping (persisted to JSON)
├── response-formatter.ts ← Splits long text for Discord's 2000 char limit
├── error-formatter.ts ← Sanitizes errors (strips keys, paths, stacks)
├── heartbeat-scheduler.ts ← setInterval timers from heartbeat.md
├── cron-scheduler.ts ← node-cron jobs from agents.md
├── hook-manager.ts ← Lifecycle hooks from agents.md
├── bootstrap-manager.ts ← First-run: validates/creates config files
├── channel-queue.ts ← Per-channel sequential processing
└── shutdown-handler.ts ← SIGTERM/SIGINT → graceful shutdown
config/
├── identity.md ← Agent name, role, specialization
├── soul.md ← Personality, tone, values
├── agents.md ← Rules, cron jobs, hooks
├── user.md ← Human's info and preferences
├── memory.md ← Long-term memory (agent-writable)
├── tools.md ← Tool configs and notes
├── heartbeat.md ← Proactive check definitions
├── boot.md ← Bootstrap parameters (optional)
└── sessions.json ← Channel → session ID map (auto-generated)
```

351
package-lock.json generated
View File

@@ -9,8 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.50",
"discord.js": "^14.25.1", "discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"node-cron": "^4.2.1" "node-cron": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
@@ -22,29 +22,6 @@
"vitest": "^4.0.18" "vitest": "^4.0.18"
} }
}, },
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.2.50",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.50.tgz",
"integrity": "sha512-zVQzJbicfTmvS6uarFQYYVYiYedKE0FgXmhiGC1oSLm6OkIbuuKM7XV4fXEFxPZHcWQc7ZYv6HA2/P5HOE7b2Q==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.34.2",
"@img/sharp-darwin-x64": "^0.34.2",
"@img/sharp-linux-arm": "^0.34.2",
"@img/sharp-linux-arm64": "^0.34.2",
"@img/sharp-linux-x64": "^0.34.2",
"@img/sharp-linuxmusl-arm64": "^0.34.2",
"@img/sharp-linuxmusl-x64": "^0.34.2",
"@img/sharp-win32-arm64": "^0.34.2",
"@img/sharp-win32-x64": "^0.34.2"
},
"peerDependencies": {
"zod": "^4.0.0"
}
},
"node_modules/@discordjs/builders": { "node_modules/@discordjs/builders": {
"version": "1.13.1", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
@@ -617,310 +594,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -1545,6 +1218,18 @@
"url": "https://github.com/discordjs/discord.js?sponsor" "url": "https://github.com/discordjs/discord.js?sponsor"
} }
}, },
"node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -2210,16 +1895,6 @@
"optional": true "optional": true
} }
} }
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View File

@@ -14,6 +14,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"discord.js": "^14.25.1", "discord.js": "^14.25.1",
"dotenv": "^17.3.1",
"node-cron": "^4.2.1" "node-cron": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,3 +1,4 @@
import "dotenv/config";
import { GatewayCore } from "./gateway-core.js"; import { GatewayCore } from "./gateway-core.js";
import { registerShutdownHandler } from "./shutdown-handler.js"; import { registerShutdownHandler } from "./shutdown-handler.js";