wolli 0.0.3 → 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 +23 -24
- package/built-in/plugins/discord/discord-chat.ts +47 -84
- package/built-in/plugins/discord/index.ts +139 -97
- package/built-in/plugins/discord/package.json +2 -2
- package/built-in/plugins/scheduler/cron.ts +131 -0
- package/built-in/plugins/scheduler/index.ts +147 -151
- package/built-in/plugins/scheduler/package.json +3 -2
- package/built-in/plugins/scheduler/scheduler-due.ts +22 -0
- package/built-in/plugins/telegram/README.md +3 -3
- package/built-in/plugins/telegram/index.ts +177 -139
- package/built-in/plugins/telegram/package.json +2 -2
- package/built-in/plugins/telegram/telegram-chat.ts +79 -155
- package/dist/cli.js +5696 -5841
- package/docs/hooks.md +103 -0
- package/docs/index.md +10 -8
- package/docs/integrations.md +78 -663
- package/docs/introduction.md +95 -0
- package/docs/plugins.md +43 -36
- package/docs/providers.md +63 -0
- package/docs/sdk.md +42 -47
- package/docs/tools.md +81 -0
- package/docs/workflows.md +170 -0
- package/package.json +1 -1
- package/built-in/plugins/scheduler/scheduler-chat.ts +0 -164
- package/docs/extensions.md +0 -2331
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Wolli
|
|
4
4
|
|
|
5
|
-
**Create
|
|
5
|
+
**Create agents that grow around a purpose.**
|
|
6
6
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](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
|
|
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,27 +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
|
-
|
|
33
|
-
|
|
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`.
|
|
35
32
|
|
|
36
33
|
```
|
|
37
34
|
Agents
|
|
38
35
|
|
|
39
|
-
→
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
○ sprout Still working out my purpose.
|
|
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.
|
|
43
39
|
|
|
44
40
|
↑/↓ browse · enter chat · tab details · type to search commands · ctrl+c quit
|
|
45
41
|
```
|
|
46
42
|
|
|
47
43
|
## How it works
|
|
48
44
|
|
|
49
|
-
- **Purpose-built.**
|
|
50
|
-
|
|
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.
|
|
51
48
|
- **Self-extending.** The agent builds itself out for its purpose. It curates its
|
|
52
|
-
own memory and authors and installs its own skills, tools,
|
|
49
|
+
own memory and authors and installs its own skills, tools, workflows, and integrations; they
|
|
53
50
|
live in its home and load on reload. The agent grows more capable at its job
|
|
54
51
|
instead of staying a fixed tool.
|
|
55
52
|
- **Persistent.** Sessions are an append-only JSONL tree, the agent's lifetime
|
|
@@ -60,7 +57,7 @@ state live under `~/.wolli`. `●` deployed and `○` forming.
|
|
|
60
57
|
|
|
61
58
|
| File | Holds |
|
|
62
59
|
| --- | --- |
|
|
63
|
-
| `SOUL.md` | Identity, authored
|
|
60
|
+
| `SOUL.md` | Identity, authored by the agent; the first line is its purpose. |
|
|
64
61
|
| `MEMORY.md` | Durable notes the agent keeps. |
|
|
65
62
|
| `USER.md` | Facts about its human. |
|
|
66
63
|
|
|
@@ -73,10 +70,10 @@ state live under `~/.wolli`. `●` deployed and `○` forming.
|
|
|
73
70
|
|
|
74
71
|
## Lifecycle
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
80
77
|
|
|
81
78
|
`wolli delete <name>` removes an agent and its state.
|
|
82
79
|
|
|
@@ -87,8 +84,10 @@ removes them with it:
|
|
|
87
84
|
|
|
88
85
|
| Type | What it adds |
|
|
89
86
|
| --- | --- |
|
|
90
|
-
| **Integrations** |
|
|
91
|
-
| **
|
|
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. |
|
|
92
91
|
| **Skills** | The Agent Skills standard. |
|
|
93
92
|
| **Prompt templates** | `/name` slash commands. |
|
|
94
93
|
| **Themes** | TUI appearance. |
|
|
@@ -101,7 +100,7 @@ Two integrations ship bundled: **Telegram** (bidirectional chat) and a
|
|
|
101
100
|
| Command | Action |
|
|
102
101
|
| --- | --- |
|
|
103
102
|
| `wolli` | Set up on first run; otherwise pick an agent and open it |
|
|
104
|
-
| `wolli new <name>` | Create
|
|
103
|
+
| `wolli new <name>` | Create a new agent and start chatting with it |
|
|
105
104
|
| `wolli <agent>` | Open a specific agent interactively |
|
|
106
105
|
| `wolli <agent> "msg" --print` | One-shot, non-interactive reply |
|
|
107
106
|
| `wolli list` | List agents |
|
|
@@ -112,7 +111,7 @@ Two integrations ship bundled: **Telegram** (bidirectional chat) and a
|
|
|
112
111
|
## Documentation
|
|
113
112
|
|
|
114
113
|
Full documentation is in [`packages/wolli/docs`](packages/wolli/docs/index.md):
|
|
115
|
-
|
|
114
|
+
workflows, integrations, tools, providers, hooks, skills, prompt templates, themes, plugins, and the SDK.
|
|
116
115
|
|
|
117
116
|
## Roadmap
|
|
118
117
|
|
|
@@ -1,87 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Discord chat
|
|
3
|
-
* each channel/DM
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const session = match
|
|
47
|
-
? await wolli.openSession(match.id)
|
|
48
|
-
: await wolli.createSession({
|
|
49
|
-
setup: async (sessionManager) => {
|
|
50
|
-
await sessionManager.appendTags(channelTag);
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// followUp: a message arriving mid-turn queues instead of interrupting.
|
|
55
|
-
void session.sendUserMessage(m.text, { deliverAs: "followUp" });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const stopTyping = (): void => {
|
|
59
|
-
if (typingTimer) {
|
|
60
|
-
clearInterval(typingTimer);
|
|
61
|
-
typingTimer = undefined;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
wolli.on("agent_start", async (_event, ctx) => {
|
|
66
|
-
const channelId = ctx.session.getTags()["discord:channel"];
|
|
67
|
-
if (!channelId) return; // not a discord-bound session
|
|
68
|
-
const sendTyping = () => {
|
|
69
|
-
void discord.call("sendTyping", { channelId }).catch(() => {});
|
|
70
|
-
};
|
|
71
|
-
sendTyping();
|
|
72
|
-
stopTyping();
|
|
73
|
-
typingTimer = setInterval(sendTyping, TYPING_INTERVAL_MS);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
wolli.on("agent_end", async ({ messages }, ctx) => {
|
|
77
|
-
stopTyping();
|
|
78
|
-
|
|
79
|
-
// Reply goes to the channel that started this turn (the producing session's tag).
|
|
80
|
-
const channelId = ctx.session.getTags()["discord:channel"];
|
|
81
|
-
if (!channelId) return; // not a discord-bound session
|
|
82
|
-
|
|
83
|
-
const text = finalAssistantText(messages as AgentMessage[]);
|
|
84
|
-
if (!text) return; // pure tool-call turn — nothing to send
|
|
85
|
-
await discord.call("sendMessage", { channelId, text });
|
|
86
|
-
});
|
|
87
|
-
}
|
|
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
|
+
});
|
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Discord transport — holds the bot token, emits a `message` event per inbound message;
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
8
|
* See README.md for setup.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import
|
|
11
|
+
import { defineIntegration, type IntegrationOnboardContext } from "wolli";
|
|
10
12
|
import { Client, Events, GatewayIntentBits, Partials, REST, Routes } from "discord.js";
|
|
11
13
|
import { Type } from "typebox";
|
|
12
14
|
|
|
13
15
|
/** Discord caps a single message at 2000 UTF-16 code units. */
|
|
14
16
|
const DISCORD_MAX_LENGTH = 2000;
|
|
15
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
|
+
|
|
16
41
|
const ONBOARD_GUIDE = [
|
|
17
42
|
"## Connect Discord",
|
|
18
43
|
"",
|
|
@@ -84,109 +109,126 @@ async function onboard(ctx: IntegrationOnboardContext): Promise<{ botToken: stri
|
|
|
84
109
|
return { botToken: token };
|
|
85
110
|
}
|
|
86
111
|
|
|
87
|
-
export default
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}),
|
|
94
127
|
}),
|
|
95
|
-
|
|
96
|
-
|
|
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({
|
|
97
134
|
channelId: Type.String(),
|
|
98
|
-
messageId: Type.String(),
|
|
99
135
|
text: Type.String(),
|
|
100
|
-
author: Type.Object({
|
|
101
|
-
id: Type.String(),
|
|
102
|
-
name: Type.Optional(Type.String()),
|
|
103
|
-
}),
|
|
104
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
|
+
},
|
|
105
151
|
},
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
id: string;
|
|
123
|
-
};
|
|
124
|
-
messageIds.push(sent.id);
|
|
125
|
-
}
|
|
126
|
-
return { messageIds };
|
|
127
|
-
},
|
|
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 };
|
|
128
168
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return { ok: true };
|
|
140
|
-
},
|
|
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 };
|
|
141
179
|
},
|
|
142
180
|
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const client = new Client({
|
|
152
|
-
intents: [
|
|
153
|
-
GatewayIntentBits.Guilds,
|
|
154
|
-
GatewayIntentBits.GuildMessages,
|
|
155
|
-
GatewayIntentBits.MessageContent,
|
|
156
|
-
GatewayIntentBits.DirectMessages,
|
|
157
|
-
],
|
|
158
|
-
partials: [Partials.Channel], // required to receive DMs
|
|
159
|
-
});
|
|
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
|
+
}
|
|
160
189
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 },
|
|
174
212
|
});
|
|
213
|
+
});
|
|
175
214
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
215
|
+
client.on(Events.Error, (err) => {
|
|
216
|
+
console.error("[discord] client error:", err.message);
|
|
217
|
+
});
|
|
179
218
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
225
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
});
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"wolli": {
|
|
7
7
|
"integrations": ["./index.ts"],
|
|
8
|
-
"
|
|
8
|
+
"workflows": ["./discord-chat.ts"]
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"discord.js": "14.26.4"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"
|
|
14
|
+
"wolli": "*"
|
|
15
15
|
}
|
|
16
16
|
}
|