wolli 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Wolli
4
4
 
5
- **Build persistent, purposeful AI agents.**
5
+ **Create agents that grow around a purpose.**
6
6
 
7
7
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
8
  [![npm](https://img.shields.io/npm/v/wolli.svg)](https://www.npmjs.com/package/wolli)
@@ -16,10 +16,7 @@
16
16
 
17
17
  > **What is my purpose?**
18
18
 
19
- Wolli lets you create purposeful agents. That purpose becomes the organizing
20
- principle of an agent's life: it remembers across sessions, runs on a schedule,
21
- acts on events, and extends itself to do its job better over time, writing its
22
- own skills, integrations, and extensions as the work demands.
19
+ Wolli lets you create agents that grow around a purpose. Each agent remembers across sessions, runs on schedules, reacts to events, and extends itself over time by writing the skills, integrations, and workflows it needs to do its job better.
23
20
 
24
21
  ## Install
25
22
 
@@ -29,16 +26,27 @@ wolli
29
26
  ```
30
27
 
31
28
  The first run sets up your provider and creates your first agent. A new agent
32
- starts in a **forming** state: it interviews you to work out its purpose and
33
- records what it learns, and does not act unattended until you deploy it. Agents and
34
- state live under `~/.wolli`.
29
+ opens by interviewing you to work out its purpose, then writes its own
30
+ `SOUL.md` its first line becomes the agent's description everywhere. Agents
31
+ and state live under `~/.wolli`.
32
+
33
+ ```
34
+ Agents
35
+
36
+ → inbox Triage my email each morning, draft replies to the routine ones, flag what needs me.
37
+ scout Watch the repos and deps we ship; when a release or CVE needs action, open an issue and ping me.
38
+ ledger Track project spend across providers, reconcile invoices weekly, warn me before a budget tips over.
39
+
40
+ ↑/↓ browse · enter chat · tab details · type to search commands · ctrl+c quit
41
+ ```
35
42
 
36
43
  ## How it works
37
44
 
38
- - **Purpose-built.** You state the agent's purpose at birth. It decides what the
39
- agent stores, when it speaks up, and what it does unattended.
45
+ - **Purpose-built.** The agent works its purpose out with you in its first
46
+ conversation and records it as the first line of its `SOUL.md`. It decides what
47
+ the agent stores, when it speaks up, and what it does unattended.
40
48
  - **Self-extending.** The agent builds itself out for its purpose. It curates its
41
- own memory and authors and installs its own skills, tools, and extensions; they
49
+ own memory and authors and installs its own skills, tools, workflows, and integrations; they
42
50
  live in its home and load on reload. The agent grows more capable at its job
43
51
  instead of staying a fixed tool.
44
52
  - **Persistent.** Sessions are an append-only JSONL tree, the agent's lifetime
@@ -49,12 +57,12 @@ state live under `~/.wolli`.
49
57
 
50
58
  | File | Holds |
51
59
  | --- | --- |
52
- | `SOUL.md` | Identity, authored at deploy. |
60
+ | `SOUL.md` | Identity, authored by the agent; the first line is its purpose. |
53
61
  | `MEMORY.md` | Durable notes the agent keeps. |
54
62
  | `USER.md` | Facts about its human. |
55
63
 
56
64
  - **Always on, locally.** A per-agent daemon supervised by launchd (macOS) or
57
- systemd (linux) runs the agent on schedules and events while your machine is on.
65
+ systemd (Linux) runs the agent on schedules and events while your machine is on.
58
66
  - **Sandboxed.** The agent runs in a sandbox by default: `srt` (Apple Seatbelt /
59
67
  bubblewrap), or optional Docker. Reaching your real machine is an explicit,
60
68
  approval-gated escalation.
@@ -62,10 +70,10 @@ state live under `~/.wolli`.
62
70
 
63
71
  ## Lifecycle
64
72
 
65
- | State | What happens |
66
- | --- | --- |
67
- | **forming** | Interviews its human and records memory. Does not act unattended. |
68
- | **deployed** | The agent has authored its purpose and `SOUL.md`, and you confirmed `/deploy`, the single human-held latch. It now runs on schedules and events. |
73
+ An agent is live from the moment it is created: its daemon is installed as an
74
+ OS service at creation and runs on schedules and events from birth. While its
75
+ `SOUL.md` is empty it interviews you to work out its purpose, then authors
76
+ `SOUL.md` itself.
69
77
 
70
78
  `wolli delete <name>` removes an agent and its state.
71
79
 
@@ -76,7 +84,10 @@ removes them with it:
76
84
 
77
85
  | Type | What it adds |
78
86
  | --- | --- |
79
- | **Extensions** | TypeScript modules: tools, commands, events, UI. |
87
+ | **Integrations** | Transports that connect external services and message channels. |
88
+ | **Workflows** | Route events into sessions and automate the agent. |
89
+ | **Tools** | Typed actions the model calls during a turn. |
90
+ | **Providers** | Model providers beyond the built-in catalog. |
80
91
  | **Skills** | The Agent Skills standard. |
81
92
  | **Prompt templates** | `/name` slash commands. |
82
93
  | **Themes** | TUI appearance. |
@@ -89,7 +100,7 @@ Two integrations ship bundled: **Telegram** (bidirectional chat) and a
89
100
  | Command | Action |
90
101
  | --- | --- |
91
102
  | `wolli` | Set up on first run; otherwise pick an agent and open it |
92
- | `wolli new <name>` | Create and birth a new agent |
103
+ | `wolli new <name>` | Create a new agent and start chatting with it |
93
104
  | `wolli <agent>` | Open a specific agent interactively |
94
105
  | `wolli <agent> "msg" --print` | One-shot, non-interactive reply |
95
106
  | `wolli list` | List agents |
@@ -100,8 +111,7 @@ Two integrations ship bundled: **Telegram** (bidirectional chat) and a
100
111
  ## Documentation
101
112
 
102
113
  Full documentation is in [`packages/wolli/docs`](packages/wolli/docs/index.md):
103
- extensions, skills, prompt templates, themes, integrations, plugins, and the SDK.
104
- The integration/extension split is covered in [INTEGRATION.md](INTEGRATION.md).
114
+ workflows, integrations, tools, providers, hooks, skills, prompt templates, themes, plugins, and the SDK.
105
115
 
106
116
  ## Roadmap
107
117
 
@@ -0,0 +1,156 @@
1
+ # Discord
2
+
3
+ Connect a wolli agent to Discord. The bot runs over Discord's gateway WebSocket,
4
+ gives each channel and DM its own wolli session, and replies in place. Setup is a
5
+ few clicks in the Discord Developer Portal plus a single token prompt.
6
+
7
+ ## How the bot behaves
8
+
9
+ Before setup, here's the part most people want to know: this bot answers
10
+ **everything it can read**. There is no `@mention` gate.
11
+
12
+ | Context | Behavior |
13
+ |---------|----------|
14
+ | **DMs** | Responds to every non-empty message. |
15
+ | **Server channels** | Responds to every non-empty message in any channel it can read. No `@mention` required — unlike some Discord agents, the bot replies to plain messages. Scope where it listens with `allowedChannelIds`. |
16
+ | **Other bots / itself** | Ignored. The bot skips its own messages and messages from any other bot, so it can't loop. |
17
+ | **Empty / media-only messages** | Skipped (there is no inbound media handling). |
18
+
19
+ While a turn runs, the bot shows Discord's **typing…** indicator in the channel and
20
+ keeps it alive until the reply lands. Replies are chunked at **2000 characters**
21
+ (Discord's per-message limit); Discord renders markdown natively, so formatting in
22
+ the agent's output comes through as-is.
23
+
24
+ If a new message arrives while the agent is mid-reply, it is **queued as a
25
+ follow-up** and answered after the current turn finishes — it does not interrupt the
26
+ in-flight response.
27
+
28
+ ## Session model
29
+
30
+ Each channel and DM gets its **own wolli session**, bound by a `discord:channel`
31
+ tag:
32
+
33
+ - **Inbound** — an incoming message is routed to the session tagged for its channel.
34
+ If none exists yet, one is created and tagged on the spot. Histories never bleed
35
+ across channels, and two channels can run in parallel.
36
+ - **Outbound** — the reply rides the producing session's tag, so the answer always
37
+ returns to the channel that started the turn — not to whoever messaged most
38
+ recently.
39
+
40
+ ## Setup
41
+
42
+ ### 1. Create a Discord application
43
+
44
+ Open the [Discord Developer Portal](https://discord.com/developers/applications) and
45
+ click **New Application**. Name it (e.g. "Wolli") and accept the terms.
46
+
47
+ ### 2. Enable the Message Content Intent
48
+
49
+ This is the critical step. Open **Bot** in the sidebar, scroll to **Privileged
50
+ Gateway Intents**, toggle **Message Content Intent** to **ON**, and **Save Changes**.
51
+
52
+ Without it, the bot still receives message events but the message text arrives
53
+ **empty**, so it can never reply. This is the single privileged intent the bot
54
+ needs — you do **not** need Server Members or Presence.
55
+
56
+ ### 3. Reset and copy the bot token
57
+
58
+ On the same **Bot** page, click **Reset Token**, complete 2FA if prompted, and copy
59
+ the token. It is shown only once — if you lose it, reset and generate a new one. Keep
60
+ it secret.
61
+
62
+ ### 4. Invite the bot
63
+
64
+ Open **OAuth2 → URL Generator** and select:
65
+
66
+ - **Scopes:** `bot`
67
+ - **Bot Permissions:** **View Channels**, **Send Messages**, **Read Message History**
68
+
69
+ Copy the generated URL at the bottom, open it, pick a server you administer, and
70
+ authorize. Only the `bot` scope is required — the `applications.commands` scope is
71
+ not needed.
72
+
73
+ ### 5. Install and onboard in wolli
74
+
75
+ ```bash
76
+ wolli <agent> plugins install ./built-in/plugins/discord
77
+ ```
78
+
79
+ In an interactive terminal this runs onboarding immediately: it prints the connect
80
+ guide, then prompts you to **paste the bot token**. Paste the token from step 3.
81
+ Wolli verifies it with a live `GET /users/@me` call and stores it. That single token
82
+ is the only value you enter.
83
+
84
+ If you installed non-interactively, onboard later with:
85
+
86
+ ```bash
87
+ wolli <agent> plugins configure discord
88
+ ```
89
+
90
+ ### 6. Restart the agent
91
+
92
+ ```bash
93
+ wolli restart <agent>
94
+ ```
95
+
96
+ This starts the gateway producer that connects to Discord. After onboarding a fresh
97
+ integration, restart the agent once so the bot comes online and begins responding.
98
+
99
+ ## Configuration reference
100
+
101
+ Configuration lives per agent in `~/.wolli/agents/<name>/integrations.json` under
102
+ `discord.default`:
103
+
104
+ ```json
105
+ {
106
+ "discord": {
107
+ "default": {
108
+ "botToken": "...",
109
+ "allowedChannelIds": ["123456789012345678"]
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ | Field | Required | Default | Purpose |
116
+ |-------|----------|---------|---------|
117
+ | `botToken` | Yes | — | The bot token. Onboarding stores it raw; a `$ENV` / `!cmd` reference placed here by hand also resolves on read. |
118
+ | `allowedChannelIds` | No | *(any channel)* | Allowlist of channel IDs the bot will respond in. Empty or absent accepts **any** channel the bot can read (logged as a warning at startup). |
119
+
120
+ Discord IDs are snowflakes — keep them as **quoted strings** in JSON, never numbers,
121
+ since a 64-bit snowflake exceeds JavaScript's safe-integer range. To grab a channel
122
+ ID, enable **Developer Mode** in Discord (Settings → Advanced), then right-click a
123
+ channel and **Copy Channel ID**.
124
+
125
+ `allowedChannelIds` is not asked during onboarding — edit `integrations.json` to set
126
+ it.
127
+
128
+ ## Not supported
129
+
130
+ The bot is deliberately focused on text chat. It does not provide:
131
+
132
+ - Mention-gating — it answers every readable message, not only `@mentions`.
133
+ - User, role, or guild allowlists — channel-level `allowedChannelIds` is the only
134
+ scope control.
135
+ - Threads or auto-threading.
136
+ - Reactions.
137
+ - Native Discord slash commands.
138
+ - Inbound or outbound media, attachments, or voice.
139
+
140
+ ## Troubleshooting
141
+
142
+ | Symptom | Fix |
143
+ |---------|-----|
144
+ | Bot is online but never replies | Enable the **Message Content Intent** (step 2). Without it the message text arrives empty and there is nothing to answer. |
145
+ | `Disallowed intents` on connect | Same cause: the Message Content Intent is not enabled in the Developer Portal. |
146
+ | No reply right after the first install/onboard | Restart the agent (`wolli restart <agent>`) so the gateway producer starts. |
147
+ | Silence in one specific channel | The bot is missing **View Channels** / **Send Messages** there, or the channel isn't in `allowedChannelIds`. Try a DM to isolate. |
148
+
149
+ ## Security
150
+
151
+ - `allowedChannelIds` is the only access gate today. With it empty, **any** channel
152
+ the bot can read is accepted — set it to lock the bot to known channels.
153
+ - There is no per-user or per-role gate. Anyone who can post in an allowed channel
154
+ can drive the agent, so keep the bot in trusted channels.
155
+ - The bot token grants full control of the bot account — never commit it or share
156
+ it. Wolli writes `integrations.json` with mode `0600`.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Discord chat routing, as one workflows file:
3
+ * - `inbound` binds each channel/DM to its own session by a `discord:channel` tag and
4
+ * delivers inbound text as a followUp. Discord has no channel commands, so every message
5
+ * routes into a session.
6
+ * - `typing` starts the "typing…" indicator on `agent_start`; the integration owns the
7
+ * keep-alive timer.
8
+ * - `reply` ships the turn's final assistant text back on `agent_end`, riding the
9
+ * producing session's `discord:channel` tag; typing stops first so a pure tool-call turn
10
+ * still clears the indicator.
11
+ */
12
+
13
+ import { wolli } from "wolli";
14
+ import discord from "./index.ts";
15
+
16
+ // m is typed from the event schema
17
+ export const inbound = discord.on("message", async (m, ctx) => {
18
+ const channelTag = { "discord:channel": m.channelId };
19
+ const [match] = await ctx.agent.findSessions(channelTag);
20
+ const session = match
21
+ ? await ctx.agent.openSession(match.id)
22
+ : await ctx.agent.createSession({
23
+ setup: (s) => s.appendTags(channelTag),
24
+ });
25
+ // followUp queues behind a running turn instead of interrupting it.
26
+ await session.sendUserMessage(m.text, { deliverAs: "followUp" });
27
+ });
28
+
29
+ export const typing = wolli.on("agent_start", async (_evt, ctx) => {
30
+ const channelId = ctx.session.getTags()["discord:channel"];
31
+ if (!channelId) return; // not a discord-bound session
32
+ await ctx.integration(discord).startTyping({ channelId });
33
+ });
34
+
35
+ export const reply = wolli.on("agent_end", async (evt, ctx) => {
36
+ const channelId = ctx.session.getTags()["discord:channel"];
37
+ if (!channelId) return; // not a discord-bound session
38
+ // Stop the typing indicator before the empty-text early return, so a pure tool-call
39
+ // turn still clears it.
40
+ await ctx.integration(discord).stopTyping({ channelId });
41
+ const text = evt.messages
42
+ .filter((m) => m.role === "assistant")
43
+ .at(-1)
44
+ ?.content.filter((c) => c.type === "text")
45
+ .map((c) => c.text)
46
+ .join("")
47
+ .trim();
48
+ if (!text) return; // a pure tool-call turn sends nothing
49
+ await ctx.integration(discord).sendMessage({ channelId, text });
50
+ });
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Discord transport — holds the bot token, emits a `message` event per inbound message;
3
+ * the routing workflows (`discord-chat.ts`, declared under `wolli.workflows`) map those
4
+ * onto sessions. Transport is the gateway
5
+ * WebSocket (discord.js `Client`); snowflake ids stay `string`. MESSAGE CONTENT is a
6
+ * privileged intent — enable it in the Developer Portal or `m.content` is empty. The
7
+ * "typing…" keep-alive timers live here, since workflow files hold no module state.
8
+ * See README.md for setup.
9
+ */
10
+
11
+ import { defineIntegration, type IntegrationOnboardContext } from "wolli";
12
+ import { Client, Events, GatewayIntentBits, Partials, REST, Routes } from "discord.js";
13
+ import { Type } from "typebox";
14
+
15
+ /** Discord caps a single message at 2000 UTF-16 code units. */
16
+ const DISCORD_MAX_LENGTH = 2000;
17
+
18
+ /** Typing indicator refresh interval — Discord's typing state lasts ~10s, so refresh a little ahead. */
19
+ const TYPING_INTERVAL_MS = 8000;
20
+
21
+ /**
22
+ * Live "typing…" keep-alive timers, keyed by channelId. Per-channel so parallel channels
23
+ * never stomp each other; cleared by `stopTyping` and by the producer's disposer on reload,
24
+ * so a reload can never orphan an interval.
25
+ */
26
+ const typingTimers = new Map<string, ReturnType<typeof setInterval>>();
27
+
28
+ function stopTypingTimer(channelId: string): void {
29
+ const timer = typingTimers.get(channelId);
30
+ if (timer) {
31
+ clearInterval(timer);
32
+ typingTimers.delete(channelId);
33
+ }
34
+ }
35
+
36
+ function clearAllTypingTimers(): void {
37
+ for (const timer of typingTimers.values()) clearInterval(timer);
38
+ typingTimers.clear();
39
+ }
40
+
41
+ const ONBOARD_GUIDE = [
42
+ "## Connect Discord",
43
+ "",
44
+ "1. Open the [Discord Developer Portal](https://discord.com/developers/applications)",
45
+ " and create a **New Application**.",
46
+ "2. Go to **Bot**, then enable the **MESSAGE CONTENT INTENT** toggle (privileged —",
47
+ " without it the bot receives empty message text).",
48
+ "3. On the **Bot** page, **Reset Token** and copy the token.",
49
+ "4. Go to **OAuth2 → URL Generator**, select the `bot` scope, copy the generated URL,",
50
+ " open it, and invite the bot to your server.",
51
+ "5. Paste the token on the next screen.",
52
+ "",
53
+ "Wolli verifies the token and stores it for this agent.",
54
+ ].join("\n");
55
+
56
+ interface DiscordAccount {
57
+ botToken: string;
58
+ allowedChannelIds?: string[];
59
+ }
60
+
61
+ /** One cached `REST` client per token for the request/response actions. */
62
+ const rests = new Map<string, REST>();
63
+ function getRest(token: string): REST {
64
+ let rest = rests.get(token);
65
+ if (!rest) {
66
+ rest = new REST({ version: "10" }).setToken(token);
67
+ rests.set(token, rest);
68
+ }
69
+ return rest;
70
+ }
71
+
72
+ /** Split into ≤2000-code-unit chunks without cutting a surrogate pair. */
73
+ function chunkText(text: string, max = DISCORD_MAX_LENGTH): string[] {
74
+ if (text.length <= max) return [text];
75
+ const chunks: string[] = [];
76
+ let i = 0;
77
+ while (i < text.length) {
78
+ let end = Math.min(i + max, text.length);
79
+ if (end < text.length) {
80
+ const code = text.charCodeAt(end - 1);
81
+ if (code >= 0xd800 && code <= 0xdbff) end -= 1;
82
+ }
83
+ chunks.push(text.slice(i, end));
84
+ i = end;
85
+ }
86
+ return chunks;
87
+ }
88
+
89
+ /** Guided setup: collect the bot token, verify it via `GET /users/@me`, return it to store. */
90
+ async function onboard(ctx: IntegrationOnboardContext): Promise<{ botToken: string } | undefined> {
91
+ ctx.ui.notify(ONBOARD_GUIDE, "info");
92
+
93
+ const entered = await ctx.ui.input("Paste the bot token from the Discord Developer Portal");
94
+ if (entered === undefined) return undefined; // cancelled
95
+ const token = entered.trim();
96
+ if (!token) {
97
+ ctx.ui.notify("No token entered.", "error");
98
+ return undefined;
99
+ }
100
+
101
+ try {
102
+ const me = (await new REST({ version: "10" }).setToken(token).get(Routes.user())) as { username: string };
103
+ ctx.ui.notify(`Verified bot ${me.username}.`, "info");
104
+ } catch (err) {
105
+ ctx.ui.notify(`Could not verify the token: ${err instanceof Error ? err.message : String(err)}`, "error");
106
+ return undefined;
107
+ }
108
+
109
+ return { botToken: token };
110
+ }
111
+
112
+ export default defineIntegration({
113
+ account: Type.Object({
114
+ botToken: Type.String(),
115
+ /** Empty/absent = allow all (logged as a warning). */
116
+ allowedChannelIds: Type.Optional(Type.Array(Type.String())),
117
+ }),
118
+ events: {
119
+ message: Type.Object({
120
+ channelId: Type.String(),
121
+ messageId: Type.String(),
122
+ text: Type.String(),
123
+ author: Type.Object({
124
+ id: Type.String(),
125
+ name: Type.Optional(Type.String()),
126
+ }),
127
+ }),
128
+ },
129
+ onboard,
130
+ actions: {
131
+ sendMessage: {
132
+ description: "Send a text message to a channel (chunked at 2000; Discord renders markdown natively).",
133
+ parameters: Type.Object({
134
+ channelId: Type.String(),
135
+ text: Type.String(),
136
+ }),
137
+ execute: async (params, ctx) => {
138
+ const { channelId, text } = params as { channelId: string; text: string };
139
+ const account = ctx.account as DiscordAccount;
140
+ const rest = getRest(account.botToken);
141
+
142
+ const messageIds: string[] = [];
143
+ for (const content of chunkText(text)) {
144
+ const sent = (await rest.post(Routes.channelMessages(channelId), { body: { content } })) as {
145
+ id: string;
146
+ };
147
+ messageIds.push(sent.id);
148
+ }
149
+ return { messageIds };
150
+ },
151
+ },
152
+ startTyping: {
153
+ description: "Show the 'typing…' indicator in a channel and keep it alive on a timer until stopTyping.",
154
+ parameters: Type.Object({
155
+ channelId: Type.String(),
156
+ }),
157
+ execute: async (params, ctx) => {
158
+ const { channelId } = params as { channelId: string };
159
+ const account = ctx.account as DiscordAccount;
160
+ const rest = getRest(account.botToken);
161
+ const send = () => {
162
+ void rest.post(Routes.channelTyping(channelId)).catch(() => {});
163
+ };
164
+ send(); // immediate first tick; the indicator would otherwise lag by the interval
165
+ stopTypingTimer(channelId); // replace any existing timer for this channel
166
+ typingTimers.set(channelId, setInterval(send, TYPING_INTERVAL_MS));
167
+ return { ok: true };
168
+ },
169
+ },
170
+ stopTyping: {
171
+ description: "Stop the 'typing…' indicator in a channel.",
172
+ parameters: Type.Object({
173
+ channelId: Type.String(),
174
+ }),
175
+ execute: async (params) => {
176
+ const { channelId } = params as { channelId: string };
177
+ stopTypingTimer(channelId);
178
+ return { ok: true };
179
+ },
180
+ },
181
+ },
182
+ run(ctx) {
183
+ const account = ctx.account as DiscordAccount;
184
+ const { botToken, allowedChannelIds } = account;
185
+ const allowAll = !allowedChannelIds || allowedChannelIds.length === 0;
186
+ if (allowAll) {
187
+ console.warn("[discord] no allowedChannelIds configured — accepting messages from ANY channel.");
188
+ }
189
+
190
+ const client = new Client({
191
+ intents: [
192
+ GatewayIntentBits.Guilds,
193
+ GatewayIntentBits.GuildMessages,
194
+ GatewayIntentBits.MessageContent,
195
+ GatewayIntentBits.DirectMessages,
196
+ ],
197
+ partials: [Partials.Channel], // required to receive DMs
198
+ });
199
+
200
+ client.on(Events.MessageCreate, (m) => {
201
+ if (m.author.id === client.user?.id) return; // skip self
202
+ if (m.author.bot) return; // skip other bots (loop prevention)
203
+ if (!allowAll && !allowedChannelIds?.includes(m.channelId)) return;
204
+ const text = m.content;
205
+ if (!text) return; // empty without the MESSAGE CONTENT intent, or media-only
206
+
207
+ ctx.emit("message", {
208
+ channelId: m.channelId,
209
+ messageId: m.id,
210
+ text,
211
+ author: { id: m.author.id, name: m.author.username },
212
+ });
213
+ });
214
+
215
+ client.on(Events.Error, (err) => {
216
+ console.error("[discord] client error:", err.message);
217
+ });
218
+
219
+ // Fire-and-forget: login never settles into a state we await.
220
+ if (!ctx.signal.aborted) {
221
+ void client.login(botToken).catch((err) => {
222
+ console.error("[discord] failed to log in:", err instanceof Error ? err.message : err);
223
+ });
224
+ }
225
+
226
+ const dispose = () => {
227
+ // Clear typing timers here so a reload can never orphan an interval.
228
+ clearAllTypingTimers();
229
+ void client.destroy();
230
+ };
231
+ ctx.signal.addEventListener("abort", dispose);
232
+ return dispose;
233
+ },
234
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "wolli-integration-discord",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "wolli": {
7
+ "integrations": ["./index.ts"],
8
+ "workflows": ["./discord-chat.ts"]
9
+ },
10
+ "dependencies": {
11
+ "discord.js": "14.26.4"
12
+ },
13
+ "peerDependencies": {
14
+ "wolli": "*"
15
+ }
16
+ }