@mindot/will 0.8.0 → 0.9.0
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/dist/channels/discord.d.ts +67 -6
- package/dist/channels/discord.js +112 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js +4 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +3174 -867
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3236 -1042
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-cS6k4uiJ.d.ts → will-DbDj_TEH.d.ts} +752 -17
- package/package.json +1 -1
- package/src/channels/discord.ts +189 -11
- package/src/channels/types.ts +90 -0
- package/src/channels/whatsapp.ts +13 -4
- package/src/cli.ts +9 -4
- package/src/cognition/agency/consequence.ts +122 -1
- package/src/cognition/agency/conversation.aim.ts +260 -0
- package/src/cognition/agency/engines/action.selector.ts +83 -2
- package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
- package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
- package/src/cognition/agency/engines/reafference.engine.ts +117 -0
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/selection.scoring.ts +33 -0
- package/src/cognition/agency/types.ts +35 -0
- package/src/cognition/cache/composition.ts +232 -0
- package/src/cognition/cache/deliberation.cache.ts +219 -0
- package/src/cognition/cache/fingerprint.ts +120 -0
- package/src/cognition/cache/types.ts +105 -0
- package/src/cognition/config.mirror.entities.ts +108 -0
- package/src/cognition/event.schemas.ts +22 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +67 -13
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +552 -131
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
- package/src/cognition/faculties/executive.engine/facet.ts +81 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +21 -1
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +167 -19
- package/src/cognition/faculties/executive.engine/types.ts +69 -0
- package/src/cognition/faculties/goal.manager.ts +94 -14
- package/src/cognition/faculties/known.entity.tracker.ts +267 -28
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/social.perception.ts +47 -3
- package/src/cognition/faculties/threat.evaluator.ts +7 -0
- package/src/cognition/faculties/working.memory.ts +10 -20
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +7 -0
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +45 -2
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/senses/audition.engine/engine.ts +404 -46
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/senses/index.ts +12 -0
- package/src/cognition/social.identity.ts +273 -0
- package/src/cognition/utilities/token.tracker.ts +58 -5
- package/src/core/orchestrator.ts +38 -0
- package/src/llm/index.ts +25 -8
- package/src/llm/routing.ts +6 -0
- package/src/llm/summarizer.ts +1 -1
- package/src/llm/wire.contracts.ts +19 -0
- package/src/pma/index.ts +67 -53
- package/src/sdk/will.ts +39 -6
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +1 -1
- package/src/stem/index.ts +79 -2
- package/src/stem/mind.ts +172 -55
- package/src/stem/tracts/outbox.writer.ts +40 -2
- package/src/cognition/faculties/executive.engine/messages.ts +0 -102
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { W as Will } from '../will-
|
|
1
|
+
import { W as Will } from '../will-DbDj_TEH.js';
|
|
2
2
|
import { C as ChannelBridge } from '../types-E9-HV-SW.js';
|
|
3
3
|
|
|
4
4
|
interface DiscordLikeChannel {
|
|
5
5
|
send(content: string): Promise<unknown>;
|
|
6
6
|
sendTyping?(): Promise<unknown>;
|
|
7
7
|
}
|
|
8
|
+
interface DiscordLikeAttachment {
|
|
9
|
+
name?: string | null;
|
|
10
|
+
contentType?: string | null;
|
|
11
|
+
size?: number;
|
|
12
|
+
url?: string;
|
|
13
|
+
}
|
|
8
14
|
interface DiscordLikeMessage {
|
|
9
15
|
content: string;
|
|
10
16
|
cleanContent?: string;
|
|
@@ -23,6 +29,16 @@ interface DiscordLikeMessage {
|
|
|
23
29
|
has(userId: string): boolean;
|
|
24
30
|
};
|
|
25
31
|
channel: DiscordLikeChannel;
|
|
32
|
+
/**
|
|
33
|
+
* Files riding with the message.
|
|
34
|
+
*
|
|
35
|
+
* discord.js hands us a `Collection`, which extends `Map` — so iterating it
|
|
36
|
+
* directly yields `[id, attachment]` PAIRS, not attachments. Typing this as a
|
|
37
|
+
* bare `Iterable` was wrong and silently produced `name: undefined` against
|
|
38
|
+
* the real client while passing every test, because the test fake injects an
|
|
39
|
+
* array. Both shapes are accepted now and normalised in `collectAttachments`.
|
|
40
|
+
*/
|
|
41
|
+
attachments?: ReadonlyMap<string, DiscordLikeAttachment> | Iterable<DiscordLikeAttachment>;
|
|
26
42
|
}
|
|
27
43
|
interface DiscordLikeClient {
|
|
28
44
|
user: {
|
|
@@ -47,14 +63,38 @@ interface DiscordLikeClient {
|
|
|
47
63
|
interface DiscordBridgeOptions {
|
|
48
64
|
/** Bot token (Discord developer portal). Unused when `client` is injected pre-logged-in. */
|
|
49
65
|
token?: string;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Channel ids the Will inhabits. Unset — or the single wildcard `'*'` — means
|
|
68
|
+
* every channel it can see, so adding it to a new channel in Discord is enough.
|
|
69
|
+
* A list restrains it to exactly those, and a message anywhere else is dropped
|
|
70
|
+
* at the bridge: the Will never perceives it and its silence there is not a choice.
|
|
71
|
+
*/
|
|
72
|
+
channels?: readonly string[];
|
|
73
|
+
/**
|
|
74
|
+
* Where the Will only perceives guild messages that @mention it. DMs are always
|
|
75
|
+
* perceived either way.
|
|
76
|
+
*
|
|
77
|
+
* `true` — everywhere
|
|
78
|
+
* `[ 'id', … ]` — only in those channels; it listens openly elsewhere
|
|
79
|
+
* omitted / `false` — nowhere
|
|
80
|
+
*
|
|
81
|
+
* The list form is what makes a wide-open roster usable: present in every channel,
|
|
82
|
+
* but a quiet participant in the busy ones.
|
|
83
|
+
*/
|
|
84
|
+
mentionOnly?: boolean | readonly string[];
|
|
54
85
|
/** Fallback channel for utterances with no reachable addressee. */
|
|
55
86
|
homeChannelId?: string;
|
|
56
87
|
/** Roster path (default: ./.will/<willId>.discord.json). */
|
|
57
88
|
rosterPath?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Read the contents of text-like attachments (.md, .txt, .json, …) into the
|
|
91
|
+
* percept, rather than only naming them. Default true.
|
|
92
|
+
*
|
|
93
|
+
* Only Discord's own CDN is ever fetched, and only up to a size cap. Set false
|
|
94
|
+
* for a bridge that should never pull remote bytes — the Will still perceives
|
|
95
|
+
* that a file arrived and can ask about it.
|
|
96
|
+
*/
|
|
97
|
+
readAttachments?: boolean;
|
|
58
98
|
/** Test / power-user seam: bring your own client; discord.js is never imported. */
|
|
59
99
|
client?: DiscordLikeClient;
|
|
60
100
|
log?: (msg: string) => void;
|
|
@@ -65,5 +105,26 @@ interface DiscordBridgeOptions {
|
|
|
65
105
|
* itself is not stopped; it simply loses this surface.
|
|
66
106
|
*/
|
|
67
107
|
declare function connectDiscord(will: Will, opts: DiscordBridgeOptions): Promise<ChannelBridge>;
|
|
108
|
+
/**
|
|
109
|
+
* Parse `WILL_DISCORD_MENTION_ONLY` into the `mentionOnly` option.
|
|
110
|
+
*
|
|
111
|
+
* Accepts a boolean OR a channel list, because "only speak when spoken to" is
|
|
112
|
+
* rarely a whole-server property — it is how you stay present in a busy channel
|
|
113
|
+
* without narrating in it.
|
|
114
|
+
*
|
|
115
|
+
* `1` / `true` / `yes` → true (everywhere)
|
|
116
|
+
* `0` / `false` / unset → false (nowhere)
|
|
117
|
+
* `123,456` → only those channels
|
|
118
|
+
*
|
|
119
|
+
* Exported so every host parses it identically; the CLI and any SDK host share
|
|
120
|
+
* this rather than each re-deriving the syntax.
|
|
121
|
+
*/
|
|
122
|
+
declare function parseMentionOnly(raw?: string): boolean | string[];
|
|
123
|
+
/**
|
|
124
|
+
* Parse `WILL_DISCORD_CHANNELS`. `*` (or unset/empty) means every channel the Will
|
|
125
|
+
* can see — being added to a channel in Discord is then all it takes. Anything else
|
|
126
|
+
* restrains it to exactly the ids listed.
|
|
127
|
+
*/
|
|
128
|
+
declare function parseChannels(raw?: string): string[] | undefined;
|
|
68
129
|
|
|
69
|
-
export { type DiscordBridgeOptions, type DiscordLikeChannel, type DiscordLikeClient, type DiscordLikeMessage, connectDiscord };
|
|
130
|
+
export { type DiscordBridgeOptions, type DiscordLikeAttachment, type DiscordLikeChannel, type DiscordLikeClient, type DiscordLikeMessage, connectDiscord, parseChannels, parseMentionOnly };
|
package/dist/channels/discord.js
CHANGED
|
@@ -56,6 +56,46 @@ var ChannelRoster = class {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// src/channels/types.ts
|
|
59
|
+
var INLINE_CHAR_CAP = 24e3;
|
|
60
|
+
var INLINE_COUNT_CAP = 4;
|
|
61
|
+
var TEXTUAL_EXT = /\.(md|markdown|txt|text|json|jsonl|csv|tsv|ya?ml|log|ini|toml)$/i;
|
|
62
|
+
function isTextual(a) {
|
|
63
|
+
const ct = a.contentType?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
64
|
+
if (ct.startsWith("text/")) return true;
|
|
65
|
+
if (ct === "application/json" || ct === "application/x-yaml") return true;
|
|
66
|
+
return TEXTUAL_EXT.test(a.name);
|
|
67
|
+
}
|
|
68
|
+
function humanSize(bytes) {
|
|
69
|
+
if (bytes == null) return "";
|
|
70
|
+
return bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
71
|
+
}
|
|
72
|
+
async function renderAttachments(attachments, speaker, fetchText) {
|
|
73
|
+
if (attachments.length === 0) return "";
|
|
74
|
+
const who = speaker ?? "someone";
|
|
75
|
+
const out = [];
|
|
76
|
+
let inlined = 0;
|
|
77
|
+
for (const a of attachments) {
|
|
78
|
+
const meta = [a.contentType, humanSize(a.size)].filter(Boolean).join(", ");
|
|
79
|
+
const label = `${a.name}${meta ? ` (${meta})` : ""}`;
|
|
80
|
+
if (!fetchText || !isTextual(a) || inlined >= INLINE_COUNT_CAP) {
|
|
81
|
+
out.push(`[${who} shared a file I have not read: ${label}]`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const body = await fetchText(a).catch(() => null);
|
|
85
|
+
if (body == null) {
|
|
86
|
+
out.push(`[${who} shared a file I could not read: ${label}]`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
inlined++;
|
|
90
|
+
const clipped = body.length > INLINE_CHAR_CAP ? `${body.slice(0, INLINE_CHAR_CAP)}
|
|
91
|
+
[\u2026 truncated \u2014 ${humanSize(body.length)} of ${humanSize(a.size ?? body.length)}]` : body;
|
|
92
|
+
out.push(`[${who} shared ${label}; its contents follow \u2014 this is a document I was handed, not something said to me]
|
|
93
|
+
---
|
|
94
|
+
${clipped}
|
|
95
|
+
---`);
|
|
96
|
+
}
|
|
97
|
+
return out.join("\n");
|
|
98
|
+
}
|
|
59
99
|
function chunkText(text, max) {
|
|
60
100
|
if (text.length <= max) return [text];
|
|
61
101
|
const chunks = [];
|
|
@@ -73,10 +113,14 @@ function chunkText(text, max) {
|
|
|
73
113
|
|
|
74
114
|
// src/channels/discord.ts
|
|
75
115
|
var DISCORD_MESSAGE_LIMIT = 2e3;
|
|
116
|
+
var DISCORD_CDN_HOSTS = /* @__PURE__ */ new Set(["cdn.discordapp.com", "media.discordapp.net"]);
|
|
117
|
+
var MAX_FETCH_BYTES = 256 * 1024;
|
|
76
118
|
async function connectDiscord(will, opts) {
|
|
77
119
|
const log = opts.log ?? ((m) => console.error(`[will:discord] ${m}`));
|
|
78
120
|
const roster = new ChannelRoster(opts.rosterPath ?? `.will/${will.id}.discord.json`);
|
|
79
|
-
const allowed = opts.channels?.length ? new Set(opts.channels) : null;
|
|
121
|
+
const allowed = opts.channels?.length && !opts.channels.includes("*") ? new Set(opts.channels) : null;
|
|
122
|
+
const mentionEverywhere = opts.mentionOnly === true;
|
|
123
|
+
const mentionIn = Array.isArray(opts.mentionOnly) && opts.mentionOnly.length ? new Set(opts.mentionOnly) : null;
|
|
80
124
|
const client = opts.client ?? await createDiscordClient();
|
|
81
125
|
let lastActiveChannelId = opts.homeChannelId ?? null;
|
|
82
126
|
client.on("messageCreate", (message) => {
|
|
@@ -88,7 +132,7 @@ async function connectDiscord(will, opts) {
|
|
|
88
132
|
const isDM = !message.guildId;
|
|
89
133
|
if (!isDM && allowed && !allowed.has(message.channelId)) return;
|
|
90
134
|
const addressed = isDM || (message.mentions?.has(self.id) ?? false);
|
|
91
|
-
if (
|
|
135
|
+
if (!addressed && (mentionEverywhere || mentionIn?.has(message.channelId))) return;
|
|
92
136
|
const entityId = `discord:${message.author.id}`;
|
|
93
137
|
const speaker = message.member?.displayName ?? message.author.displayName ?? message.author.username;
|
|
94
138
|
roster.record({
|
|
@@ -100,15 +144,64 @@ async function connectDiscord(will, opts) {
|
|
|
100
144
|
if (!isDM) lastActiveChannelId = message.channelId;
|
|
101
145
|
if (addressed) await message.channel.sendTyping?.().catch(() => {
|
|
102
146
|
});
|
|
103
|
-
const
|
|
104
|
-
|
|
147
|
+
const said = (message.cleanContent || message.content).trim();
|
|
148
|
+
const files = collectAttachments(message);
|
|
149
|
+
if (!said && files.length === 0) return;
|
|
150
|
+
const shared = await renderAttachments(
|
|
151
|
+
files,
|
|
152
|
+
speaker,
|
|
153
|
+
opts.readAttachments === false ? void 0 : fetchAttachmentText
|
|
154
|
+
);
|
|
155
|
+
const text = [said, shared].filter(Boolean).join("\n");
|
|
105
156
|
await will.perceive({
|
|
106
157
|
text,
|
|
107
158
|
from: entityId,
|
|
108
159
|
thread: `discord:${message.channelId}`,
|
|
160
|
+
// `isDM` has been computed on every inbound since this bridge shipped and
|
|
161
|
+
// used only to pick a roster field. It is the one fact that makes a room
|
|
162
|
+
// the right or wrong place to say something, and the mind never saw it —
|
|
163
|
+
// which is how a follow-up promised in a DM went out to #general.
|
|
164
|
+
direct: isDM,
|
|
109
165
|
...speaker ? { speaker } : {}
|
|
110
166
|
});
|
|
111
167
|
}
|
|
168
|
+
function collectAttachments(message) {
|
|
169
|
+
if (!message.attachments) return [];
|
|
170
|
+
const source = message.attachments;
|
|
171
|
+
const items = typeof source.values === "function" ? source.values() : message.attachments;
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const a of items)
|
|
174
|
+
out.push({
|
|
175
|
+
name: a.name ?? "unnamed",
|
|
176
|
+
...a.contentType ? { contentType: a.contentType } : {},
|
|
177
|
+
...a.size != null ? { size: a.size } : {},
|
|
178
|
+
...a.url ? { url: a.url } : {}
|
|
179
|
+
});
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
async function fetchAttachmentText(a) {
|
|
183
|
+
if (!a.url || !isTextual(a)) return null;
|
|
184
|
+
let host;
|
|
185
|
+
try {
|
|
186
|
+
host = new URL(a.url).hostname;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (!DISCORD_CDN_HOSTS.has(host)) {
|
|
191
|
+
log(`refusing to fetch attachment '${a.name}' from non-CDN host ${host}`);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (a.size != null && a.size > MAX_FETCH_BYTES) {
|
|
195
|
+
log(`attachment '${a.name}' is ${a.size} bytes \u2014 naming it without reading`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const res = await fetch(a.url, { signal: AbortSignal.timeout(1e4) });
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
log(`attachment '${a.name}' fetch failed: ${res.status}`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return (await res.text()).slice(0, MAX_FETCH_BYTES);
|
|
204
|
+
}
|
|
112
205
|
let closed = false;
|
|
113
206
|
will.on("message", (m) => {
|
|
114
207
|
if (!closed) void deliver(m);
|
|
@@ -116,7 +209,8 @@ async function connectDiscord(will, opts) {
|
|
|
116
209
|
async function deliver(m) {
|
|
117
210
|
const peer = m.to ? roster.resolve(m.to) : void 0;
|
|
118
211
|
const chunks = chunkText(m.content, DISCORD_MESSAGE_LIMIT);
|
|
119
|
-
const
|
|
212
|
+
const replyTo = m.thread?.startsWith("discord:") ? m.thread.slice("discord:".length) : void 0;
|
|
213
|
+
const channelIds = [replyTo, peer?.lastChannelId, peer?.dmChannelId, opts.homeChannelId ?? void 0, lastActiveChannelId ?? void 0];
|
|
120
214
|
for (const id of channelIds) {
|
|
121
215
|
if (!id) continue;
|
|
122
216
|
try {
|
|
@@ -187,7 +281,19 @@ async function createDiscordClient() {
|
|
|
187
281
|
// DMs arrive on uncached channels
|
|
188
282
|
});
|
|
189
283
|
}
|
|
284
|
+
function parseMentionOnly(raw) {
|
|
285
|
+
const v = raw?.trim();
|
|
286
|
+
if (!v) return false;
|
|
287
|
+
if (/^(1|true|yes)$/i.test(v)) return true;
|
|
288
|
+
if (/^(0|false|no)$/i.test(v)) return false;
|
|
289
|
+
const ids = v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
290
|
+
return ids.length ? ids : false;
|
|
291
|
+
}
|
|
292
|
+
function parseChannels(raw) {
|
|
293
|
+
const ids = raw?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
294
|
+
return ids?.length ? ids : void 0;
|
|
295
|
+
}
|
|
190
296
|
|
|
191
|
-
export { connectDiscord };
|
|
297
|
+
export { connectDiscord, parseChannels, parseMentionOnly };
|
|
192
298
|
//# sourceMappingURL=discord.js.map
|
|
193
299
|
//# sourceMappingURL=discord.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/channels/roster.ts","../../src/channels/types.ts","../../src/channels/discord.ts"],"names":[],"mappings":"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","file":"discord.js","sourcesContent":["// ─────────────────────────────────────────────────────────────\n// src/channels/roster.ts — who the Will knows on a platform, and where\n// ─────────────────────────────────────────────────────────────\n//\n// The mind knows *entities*; a platform knows user ids and channels. The roster\n// is the durable seam between them: for each entity the Will has met on a\n// channel it records how to reach them again — so a *proactive* utterance\n// (`message.to` from the mind's own initiative) can find its person after a\n// restart, not just within one session.\n//\n// It persists as a small JSON file next to the PMA artifact. Writes are\n// throttled (the file is advisory routing state, not cognition — losing the\n// last few seconds costs a fallback delivery, never memory).\n// ─────────────────────────────────────────────────────────────\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/** How to reach one entity on the platform. */\nexport interface RosterEntry {\n /** The mind-side entity id, e.g. 'discord:80351110224678912'. */\n entityId: string\n /** The platform-side user id. */\n userId: string\n /** Last display name seen (advisory — the *learned* name lives in the mind). */\n displayName?: string\n /** DM channel id, once one is known. */\n dmChannelId?: string\n /** Last shared (guild) channel this entity spoke in. */\n lastChannelId?: string\n /** Epoch ms of the last message seen from them. */\n lastSeenAt: number\n}\n\nconst FLUSH_MS = 2_000\n\nexport class ChannelRoster {\n private entries = new Map<string, RosterEntry>()\n private dirty = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor( private readonly path: string ) {\n if( existsSync( path ) ){\n try {\n const raw = JSON.parse( readFileSync( path, 'utf8') ) as RosterEntry[]\n for( const e of Array.isArray( raw ) ? raw : [] ) this.entries.set( e.entityId, e )\n }\n catch { /* a corrupt roster is not worth failing a boot over — start fresh */ }\n }\n }\n\n /** Upsert what we just learned about an entity; schedules a throttled flush. */\n record( update: { entityId: string; userId: string } & Partial<Omit<RosterEntry, 'entityId' | 'userId'>> ): RosterEntry {\n const prev = this.entries.get( update.entityId )\n const next: RosterEntry = {\n lastSeenAt: Date.now(),\n ...prev,\n ...Object.fromEntries( Object.entries( update ).filter( ( [ , v ] ) => v !== undefined ) ) as typeof update,\n }\n this.entries.set( next.entityId, next )\n this.dirty = true\n if( !this.timer ){\n this.timer = setTimeout( () => { this.timer = null; this.flush() }, FLUSH_MS )\n this.timer.unref?.()\n }\n return next\n }\n\n resolve( entityId: string ): RosterEntry | undefined {\n return this.entries.get( entityId )\n }\n\n all(): RosterEntry[] {\n return [ ...this.entries.values() ]\n }\n\n /** Write to disk now (no-op when clean). Called by bridges on close. */\n flush(): void {\n if( !this.dirty ) return\n try {\n mkdirSync( dirname( this.path ), { recursive: true } )\n writeFileSync( this.path, JSON.stringify( this.all(), null, 2 ) )\n this.dirty = false\n }\n catch { /* advisory state — never take the mind down over it */ }\n }\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/types.ts — the channel-bridge contract\n// ─────────────────────────────────────────────────────────────\n//\n// A channel bridge puts a Will *in a place where people already are* (Discord,\n// Telegram, Slack, …). It is a host surface, not a cognition surface: it turns\n// platform messages into `perceive` stimuli and delivers the Will's projected\n// utterances back — nothing more. The paradigm survives the crossing:\n//\n// • every platform user is an entity the Will comes to know (`from`),\n// with a *learned* name (`speaker`) — never a placeholder;\n// • every platform channel/DM is a conversation thread (`thread`);\n// • the Will decides when to speak. Silence is a valid outcome, so a\n// bridge never fabricates a reply and never times a message out into\n// an error.\n//\n// Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):\n// they wrap the SDK facade, not the stem.\n// ─────────────────────────────────────────────────────────────\n\n/** A running connection between one Will and one platform. */\nexport interface ChannelBridge {\n /** Platform kind, e.g. 'discord'. */\n readonly kind: string\n /** Connect and start relaying. Resolves once the bridge is live. */\n start(): Promise<void>\n /** Disconnect and release resources. Idempotent. */\n close(): Promise<void>\n}\n\n/** Split a message into platform-sized chunks on natural boundaries. */\nexport function chunkText( text: string, max: number ): string[] {\n if( text.length <= max ) return [ text ]\n const chunks: string[] = []\n let rest = text\n while( rest.length > max ){\n // Prefer a paragraph break, then a line break, then a space — else hard-cut.\n const window = rest.slice( 0, max )\n const cut = Math.max( window.lastIndexOf('\\n\\n'), window.lastIndexOf('\\n'), window.lastIndexOf(' ') )\n const at = cut > max * 0.5 ? cut : max\n chunks.push( rest.slice( 0, at ).trimEnd() )\n rest = rest.slice( at ).trimStart()\n }\n if( rest ) chunks.push( rest )\n return chunks\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/discord.ts — a Will present in a Discord server\n// ─────────────────────────────────────────────────────────────\n//\n// The bridge relays both directions of the paradigm and nothing else:\n//\n// inbound guild/DM message → will.perceive({ from, speaker, text, thread })\n// — every author is `discord:<userId>` (stable across guilds), the\n// display name is *learned* by the mind, and each Discord channel\n// is its own conversation thread.\n// outbound will.on('message') → the addressee's last shared channel, else\n// their DM, else the home channel. Proactive utterances (the mind\n// speaking first) route the same way — that is the point.\n//\n// The Will decides when to speak. There is no command prefix and no forced\n// reply: unaddressed chatter is perceived (salience-scored by audition) and\n// silence is a valid outcome. `mentionOnly` narrows perception for busy\n// servers; it does not turn the bridge into an ask() surface.\n//\n// discord.js is imported lazily inside `createDiscordClient` — tests (and any\n// host that brings its own client) inject `client`, and the structural\n// `DiscordLikeClient` type keeps the dependency out of the type graph.\n// ─────────────────────────────────────────────────────────────\n\nimport type { Will, WillMessage } from '#sdk/will'\nimport { ChannelRoster } from '#channels/roster'\nimport { chunkText, type ChannelBridge } from '#channels/types'\n\nconst DISCORD_MESSAGE_LIMIT = 2000\n\n// ── The slice of discord.js the bridge actually uses (structural) ───────────\n\nexport interface DiscordLikeChannel {\n send( content: string ): Promise<unknown>\n sendTyping?(): Promise<unknown>\n}\n\nexport interface DiscordLikeMessage {\n content: string\n cleanContent?: string\n channelId: string\n guildId?: string | null\n author: { id: string; bot?: boolean; username?: string; displayName?: string }\n member?: { displayName?: string } | null\n mentions?: { has( userId: string ): boolean }\n channel: DiscordLikeChannel\n}\n\nexport interface DiscordLikeClient {\n user: { id: string; setPresence?( p: unknown ): void } | null\n /** discord.js ≥14.22; polled so we needn't subscribe to the deprecated `ready`. */\n isReady?(): boolean\n on( event: 'messageCreate', fn: ( m: DiscordLikeMessage ) => void ): unknown\n once( event: string, fn: () => void ): unknown\n login( token: string ): Promise<unknown>\n destroy(): Promise<unknown> | void\n channels: { fetch( id: string ): Promise<unknown> }\n users: { fetch( id: string ): Promise<{ send( content: string ): Promise<unknown> }> }\n}\n\n// ── Options ──────────────────────────────────────────────────────────────────\n\nexport interface DiscordBridgeOptions {\n /** Bot token (Discord developer portal). Unused when `client` is injected pre-logged-in. */\n token?: string\n /** Channel ids the Will inhabits. Unset = every channel it can see. */\n channels?: string[]\n /** Perceive guild messages only when the Will is @mentioned (DMs always perceived). */\n mentionOnly?: boolean\n /** Fallback channel for utterances with no reachable addressee. */\n homeChannelId?: string\n /** Roster path (default: ./.will/<willId>.discord.json). */\n rosterPath?: string\n /** Test / power-user seam: bring your own client; discord.js is never imported. */\n client?: DiscordLikeClient\n log?: ( msg: string ) => void\n}\n\n// ── The bridge ───────────────────────────────────────────────────────────────\n\n/**\n * Connect a Will to Discord. Resolves once the bridge is live (logged in and\n * relaying). Close it via the returned `ChannelBridge.close()` — the Will\n * itself is not stopped; it simply loses this surface.\n */\nexport async function connectDiscord( will: Will, opts: DiscordBridgeOptions ): Promise<ChannelBridge> {\n const log = opts.log ?? ( ( m: string ) => console.error(`[will:discord] ${ m }`) )\n const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.discord.json`)\n const allowed = opts.channels?.length ? new Set( opts.channels ) : null\n\n const client = opts.client ?? await createDiscordClient()\n\n /** The most recently active allowed channel — last-resort proactive target. */\n let lastActiveChannelId: string | null = opts.homeChannelId ?? null\n\n // ── inbound: platform message → stimulus ──────────────────────────────────\n client.on('messageCreate', message => { void onMessage( message ) } )\n\n async function onMessage( message: DiscordLikeMessage ): Promise<void> {\n const self = client.user\n if( !self || message.author.id === self.id || message.author.bot ) return\n\n const isDM = !message.guildId\n if( !isDM && allowed && !allowed.has( message.channelId ) ) return\n\n const addressed = isDM || ( message.mentions?.has( self.id ) ?? false )\n if( opts.mentionOnly && !addressed ) return\n\n const entityId = `discord:${ message.author.id }`\n const speaker = message.member?.displayName ?? message.author.displayName ?? message.author.username\n\n roster.record( {\n entityId,\n userId: message.author.id,\n ...( speaker ? { displayName: speaker } : {} ),\n ...( isDM ? { dmChannelId: message.channelId } : { lastChannelId: message.channelId } ),\n } )\n if( !isDM ) lastActiveChannelId = message.channelId\n\n // Being addressed is the one moment a presence cue is honest — the mind\n // may still choose silence, and typing expires on its own.\n if( addressed ) await message.channel.sendTyping?.().catch( () => {} )\n\n const text = message.cleanContent || message.content\n if( !text.trim() ) return\n\n await will.perceive( {\n text,\n from: entityId,\n thread: `discord:${ message.channelId }`,\n ...( speaker ? { speaker } : {} ),\n } )\n }\n\n // ── outbound: projected utterance → the addressee ─────────────────────────\n // The facade has no off(); the bridge gates its handler on `closed` instead.\n let closed = false\n will.on('message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )\n\n async function deliver( m: WillMessage ): Promise<void> {\n const peer = m.to ? roster.resolve( m.to ) : undefined\n const chunks = chunkText( m.content, DISCORD_MESSAGE_LIMIT )\n\n // Preference order: where we last shared a room → their DM → home channel.\n const channelIds = [ peer?.lastChannelId, peer?.dmChannelId, opts.homeChannelId ?? undefined, lastActiveChannelId ?? undefined ]\n for( const id of channelIds ){\n if( !id ) continue\n try {\n const channel = await client.channels.fetch( id ) as DiscordLikeChannel | null\n if( !channel?.send ) continue\n for( const chunk of chunks ) await channel.send( chunk )\n return\n }\n catch { /* try the next route */ }\n }\n if( peer ){\n try {\n const user = await client.users.fetch( peer.userId )\n for( const chunk of chunks ) await user.send( chunk )\n return\n }\n catch { /* fall through */ }\n }\n log(`no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)`)\n }\n\n // ── lifecycle ──────────────────────────────────────────────────────────────\n const bridge: ChannelBridge = {\n kind: 'discord',\n async start(): Promise<void> {\n if( !client.user ){\n // discord.js ≥14.22 renamed `ready` → `clientReady`. Subscribing to the\n // old name is what triggers its DeprecationWarning, so we take the new\n // name and poll `isReady()` for older builds rather than listening.\n const ready = new Promise<void>( resolve => {\n let poll: ReturnType<typeof setInterval> | null = null\n const done = (): void => { if( poll ) clearInterval( poll ); resolve() }\n client.once('clientReady', done )\n poll = setInterval( () => { if( client.isReady?.() ) done() }, 100 )\n poll.unref?.()\n } )\n await client.login( opts.token ?? '')\n await ready\n }\n log(`${ will.name } is present on Discord as user ${ client.user?.id }`)\n },\n async close(): Promise<void> {\n if( closed ) return\n closed = true\n roster.flush()\n await Promise.resolve( client.destroy() ).catch( () => {} )\n },\n }\n return bridge\n}\n\n/** Build a real discord.js client (lazy import keeps it out of non-Discord hosts). */\nasync function createDiscordClient(): Promise<DiscordLikeClient> {\n let mod: typeof import('discord.js')\n try { mod = await import('discord.js') }\n catch {\n throw new Error('discord.js is not installed (it is an optionalDependency) — run `bun add discord.js` / `npm i discord.js` and retry.')\n }\n const { Client, GatewayIntentBits, Partials } = mod\n return new Client( {\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [ Partials.Channel ], // DMs arrive on uncached channels\n } ) as unknown as DiscordLikeClient\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/channels/roster.ts","../../src/channels/types.ts","../../src/channels/discord.ts"],"names":[],"mappings":"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","file":"discord.js","sourcesContent":["// ─────────────────────────────────────────────────────────────\n// src/channels/roster.ts — who the Will knows on a platform, and where\n// ─────────────────────────────────────────────────────────────\n//\n// The mind knows *entities*; a platform knows user ids and channels. The roster\n// is the durable seam between them: for each entity the Will has met on a\n// channel it records how to reach them again — so a *proactive* utterance\n// (`message.to` from the mind's own initiative) can find its person after a\n// restart, not just within one session.\n//\n// It persists as a small JSON file next to the PMA artifact. Writes are\n// throttled (the file is advisory routing state, not cognition — losing the\n// last few seconds costs a fallback delivery, never memory).\n// ─────────────────────────────────────────────────────────────\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/** How to reach one entity on the platform. */\nexport interface RosterEntry {\n /** The mind-side entity id, e.g. 'discord:80351110224678912'. */\n entityId: string\n /** The platform-side user id. */\n userId: string\n /** Last display name seen (advisory — the *learned* name lives in the mind). */\n displayName?: string\n /** DM channel id, once one is known. */\n dmChannelId?: string\n /** Last shared (guild) channel this entity spoke in. */\n lastChannelId?: string\n /** Epoch ms of the last message seen from them. */\n lastSeenAt: number\n}\n\nconst FLUSH_MS = 2_000\n\nexport class ChannelRoster {\n private entries = new Map<string, RosterEntry>()\n private dirty = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor( private readonly path: string ) {\n if( existsSync( path ) ){\n try {\n const raw = JSON.parse( readFileSync( path, 'utf8') ) as RosterEntry[]\n for( const e of Array.isArray( raw ) ? raw : [] ) this.entries.set( e.entityId, e )\n }\n catch { /* a corrupt roster is not worth failing a boot over — start fresh */ }\n }\n }\n\n /** Upsert what we just learned about an entity; schedules a throttled flush. */\n record( update: { entityId: string; userId: string } & Partial<Omit<RosterEntry, 'entityId' | 'userId'>> ): RosterEntry {\n const prev = this.entries.get( update.entityId )\n const next: RosterEntry = {\n lastSeenAt: Date.now(),\n ...prev,\n ...Object.fromEntries( Object.entries( update ).filter( ( [ , v ] ) => v !== undefined ) ) as typeof update,\n }\n this.entries.set( next.entityId, next )\n this.dirty = true\n if( !this.timer ){\n this.timer = setTimeout( () => { this.timer = null; this.flush() }, FLUSH_MS )\n this.timer.unref?.()\n }\n return next\n }\n\n resolve( entityId: string ): RosterEntry | undefined {\n return this.entries.get( entityId )\n }\n\n all(): RosterEntry[] {\n return [ ...this.entries.values() ]\n }\n\n /** Write to disk now (no-op when clean). Called by bridges on close. */\n flush(): void {\n if( !this.dirty ) return\n try {\n mkdirSync( dirname( this.path ), { recursive: true } )\n writeFileSync( this.path, JSON.stringify( this.all(), null, 2 ) )\n this.dirty = false\n }\n catch { /* advisory state — never take the mind down over it */ }\n }\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/types.ts — the channel-bridge contract\n// ─────────────────────────────────────────────────────────────\n//\n// A channel bridge puts a Will *in a place where people already are* (Discord,\n// Telegram, Slack, …). It is a host surface, not a cognition surface: it turns\n// platform messages into `perceive` stimuli and delivers the Will's projected\n// utterances back — nothing more. The paradigm survives the crossing:\n//\n// • every platform user is an entity the Will comes to know (`from`),\n// with a *learned* name (`speaker`) — never a placeholder;\n// • every platform channel/DM is a conversation thread (`thread`);\n// • the Will decides when to speak. Silence is a valid outcome, so a\n// bridge never fabricates a reply and never times a message out into\n// an error.\n//\n// Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):\n// they wrap the SDK facade, not the stem.\n// ─────────────────────────────────────────────────────────────\n\n/** A running connection between one Will and one platform. */\nexport interface ChannelBridge {\n /** Platform kind, e.g. 'discord'. */\n readonly kind: string\n /** Connect and start relaying. Resolves once the bridge is live. */\n start(): Promise<void>\n /** Disconnect and release resources. Idempotent. */\n close(): Promise<void>\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────────\n//\n// People hand over documents as well as speech, and some platforms *manufacture*\n// them: Discord silently turns a long pasted markdown block into a `.md` upload.\n// A bridge that reads only the text body sees such a message as empty and — worse\n// — as nothing at all, so the person appears to have gone silent.\n//\n// What a bridge does with these is deliberately modest. A named-but-unread file\n// is already a percept the Will can act on (\"what's in it?\"), which is the\n// paradigm-correct outcome and strictly better than silence. Inlining text is an\n// upgrade on top, never a precondition.\n\n/** One file riding along with a platform message. */\nexport interface ChannelAttachment {\n name: string\n contentType?: string\n size?: number\n url?: string\n}\n\n/** Per-attachment inline budget. A 2 MB doc must not enter working memory whole. */\nconst INLINE_CHAR_CAP = 24_000\n/** How many text attachments to inline from one message. */\nconst INLINE_COUNT_CAP = 4\n\nconst TEXTUAL_EXT = /\\.(md|markdown|txt|text|json|jsonl|csv|tsv|ya?ml|log|ini|toml)$/i\n\n/** Is this something we can meaningfully read as text? */\nexport function isTextual( a: ChannelAttachment ): boolean {\n const ct = a.contentType?.split(';')[0]?.trim().toLowerCase() ?? ''\n if( ct.startsWith('text/') ) return true\n if( ct === 'application/json' || ct === 'application/x-yaml' ) return true\n // Discord's own markdown uploads arrive as text/plain, but trust the extension\n // too — content types from platforms are advisory at best.\n return TEXTUAL_EXT.test( a.name )\n}\n\nfunction humanSize( bytes?: number ): string {\n if( bytes == null ) return ''\n return bytes < 1024 ? `${ bytes } B`\n : bytes < 1024 * 1024 ? `${ ( bytes / 1024 ).toFixed( 1 ) } KB`\n : `${ ( bytes / 1024 / 1024 ).toFixed( 1 ) } MB`\n}\n\n/**\n * Render attachments into perceivable text.\n *\n * `fetchText` is supplied by the bridge, not by this module — the decision about\n * which hosts are safe to fetch from is platform knowledge, and a helper that\n * fetched arbitrary URLs found in inbound messages would be an open redirect\n * into the Will's perception. Omit it and attachments are named, never read.\n *\n * Inlined content is untrusted, exactly like message text — more so, since a\n * document is long, structured, and looks authoritative, which is the shape of\n * an effective injection. It is fenced and labelled as shared content so the\n * mind reads it as something it was handed, not as something it was told.\n */\nexport async function renderAttachments(\n attachments: ChannelAttachment[],\n speaker: string | undefined,\n fetchText?: ( a: ChannelAttachment ) => Promise<string | null>,\n): Promise<string> {\n if( attachments.length === 0 ) return ''\n const who = speaker ?? 'someone'\n const out: string[] = []\n let inlined = 0\n\n for( const a of attachments ){\n const meta = [ a.contentType, humanSize( a.size ) ].filter( Boolean ).join(', ')\n const label = `${ a.name }${ meta ? ` (${ meta })` : '' }`\n\n if( !fetchText || !isTextual( a ) || inlined >= INLINE_COUNT_CAP ){\n out.push(`[${ who } shared a file I have not read: ${ label }]`)\n continue\n }\n\n const body = await fetchText( a ).catch( () => null )\n if( body == null ){\n out.push(`[${ who } shared a file I could not read: ${ label }]`)\n continue\n }\n inlined++\n const clipped = body.length > INLINE_CHAR_CAP\n ? `${ body.slice( 0, INLINE_CHAR_CAP ) }\\n[… truncated — ${ humanSize( body.length ) } of ${ humanSize( a.size ?? body.length ) }]`\n : body\n out.push(`[${ who } shared ${ label }; its contents follow — this is a document I was handed, not something said to me]\\n---\\n${ clipped }\\n---`)\n }\n return out.join('\\n')\n}\n\n/** Split a message into platform-sized chunks on natural boundaries. */\nexport function chunkText( text: string, max: number ): string[] {\n if( text.length <= max ) return [ text ]\n const chunks: string[] = []\n let rest = text\n while( rest.length > max ){\n // Prefer a paragraph break, then a line break, then a space — else hard-cut.\n const window = rest.slice( 0, max )\n const cut = Math.max( window.lastIndexOf('\\n\\n'), window.lastIndexOf('\\n'), window.lastIndexOf(' ') )\n const at = cut > max * 0.5 ? cut : max\n chunks.push( rest.slice( 0, at ).trimEnd() )\n rest = rest.slice( at ).trimStart()\n }\n if( rest ) chunks.push( rest )\n return chunks\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/discord.ts — a Will present in a Discord server\n// ─────────────────────────────────────────────────────────────\n//\n// The bridge relays both directions of the paradigm and nothing else:\n//\n// inbound guild/DM message → will.perceive({ from, speaker, text, thread })\n// — every author is `discord:<userId>` (stable across guilds), the\n// display name is *learned* by the mind, and each Discord channel\n// is its own conversation thread.\n// outbound will.on('message') → the addressee's last shared channel, else\n// their DM, else the home channel. Proactive utterances (the mind\n// speaking first) route the same way — that is the point.\n//\n// The Will decides when to speak. There is no command prefix and no forced\n// reply: unaddressed chatter is perceived (salience-scored by audition) and\n// silence is a valid outcome. `mentionOnly` narrows perception for busy\n// servers; it does not turn the bridge into an ask() surface.\n//\n// discord.js is imported lazily inside `createDiscordClient` — tests (and any\n// host that brings its own client) inject `client`, and the structural\n// `DiscordLikeClient` type keeps the dependency out of the type graph.\n// ─────────────────────────────────────────────────────────────\n\nimport type { Will, WillMessage } from '#sdk/will'\nimport { ChannelRoster } from '#channels/roster'\nimport { chunkText, renderAttachments, isTextual, type ChannelBridge, type ChannelAttachment } from '#channels/types'\n\nconst DISCORD_MESSAGE_LIMIT = 2000\n\n/**\n * The only hosts the bridge will fetch attachment bodies from.\n *\n * Deliberately an allowlist of Discord's own CDN. An inbound message is\n * untrusted input; following URLs it names would turn perception into an open\n * redirect, and a `url` field is no more trustworthy than the message text.\n */\nconst DISCORD_CDN_HOSTS = new Set( [ 'cdn.discordapp.com', 'media.discordapp.net' ] )\n\n/** Refuse to pull a large file into a percept — the cap in renderAttachments\n * bounds what is *kept*, this bounds what is fetched at all. */\nconst MAX_FETCH_BYTES = 256 * 1024\n\n// ── The slice of discord.js the bridge actually uses (structural) ───────────\n\nexport interface DiscordLikeChannel {\n send( content: string ): Promise<unknown>\n sendTyping?(): Promise<unknown>\n}\n\nexport interface DiscordLikeAttachment {\n name?: string | null\n contentType?: string | null\n size?: number\n url?: string\n}\n\nexport interface DiscordLikeMessage {\n content: string\n cleanContent?: string\n channelId: string\n guildId?: string | null\n author: { id: string; bot?: boolean; username?: string; displayName?: string }\n member?: { displayName?: string } | null\n mentions?: { has( userId: string ): boolean }\n channel: DiscordLikeChannel\n /**\n * Files riding with the message.\n *\n * discord.js hands us a `Collection`, which extends `Map` — so iterating it\n * directly yields `[id, attachment]` PAIRS, not attachments. Typing this as a\n * bare `Iterable` was wrong and silently produced `name: undefined` against\n * the real client while passing every test, because the test fake injects an\n * array. Both shapes are accepted now and normalised in `collectAttachments`.\n */\n attachments?: ReadonlyMap<string, DiscordLikeAttachment> | Iterable<DiscordLikeAttachment>\n}\n\nexport interface DiscordLikeClient {\n user: { id: string; setPresence?( p: unknown ): void } | null\n /** discord.js ≥14.22; polled so we needn't subscribe to the deprecated `ready`. */\n isReady?(): boolean\n on( event: 'messageCreate', fn: ( m: DiscordLikeMessage ) => void ): unknown\n once( event: string, fn: () => void ): unknown\n login( token: string ): Promise<unknown>\n destroy(): Promise<unknown> | void\n channels: { fetch( id: string ): Promise<unknown> }\n users: { fetch( id: string ): Promise<{ send( content: string ): Promise<unknown> }> }\n}\n\n// ── Options ──────────────────────────────────────────────────────────────────\n\nexport interface DiscordBridgeOptions {\n /** Bot token (Discord developer portal). Unused when `client` is injected pre-logged-in. */\n token?: string\n /**\n * Channel ids the Will inhabits. Unset — or the single wildcard `'*'` — means\n * every channel it can see, so adding it to a new channel in Discord is enough.\n * A list restrains it to exactly those, and a message anywhere else is dropped\n * at the bridge: the Will never perceives it and its silence there is not a choice.\n */\n channels?: readonly string[]\n /**\n * Where the Will only perceives guild messages that @mention it. DMs are always\n * perceived either way.\n *\n * `true` — everywhere\n * `[ 'id', … ]` — only in those channels; it listens openly elsewhere\n * omitted / `false` — nowhere\n *\n * The list form is what makes a wide-open roster usable: present in every channel,\n * but a quiet participant in the busy ones.\n */\n mentionOnly?: boolean | readonly string[]\n /** Fallback channel for utterances with no reachable addressee. */\n homeChannelId?: string\n /** Roster path (default: ./.will/<willId>.discord.json). */\n rosterPath?: string\n /**\n * Read the contents of text-like attachments (.md, .txt, .json, …) into the\n * percept, rather than only naming them. Default true.\n *\n * Only Discord's own CDN is ever fetched, and only up to a size cap. Set false\n * for a bridge that should never pull remote bytes — the Will still perceives\n * that a file arrived and can ask about it.\n */\n readAttachments?: boolean\n /** Test / power-user seam: bring your own client; discord.js is never imported. */\n client?: DiscordLikeClient\n log?: ( msg: string ) => void\n}\n\n// ── The bridge ───────────────────────────────────────────────────────────────\n\n/**\n * Connect a Will to Discord. Resolves once the bridge is live (logged in and\n * relaying). Close it via the returned `ChannelBridge.close()` — the Will\n * itself is not stopped; it simply loses this surface.\n */\nexport async function connectDiscord( will: Will, opts: DiscordBridgeOptions ): Promise<ChannelBridge> {\n const log = opts.log ?? ( ( m: string ) => console.error(`[will:discord] ${ m }`) )\n const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.discord.json`)\n // `null` = everywhere. An explicit '*' reads the same as omitting the list, so a\n // host can say \"all channels\" out loud instead of by leaving a variable blank.\n const allowed = opts.channels?.length && !opts.channels.includes('*')\n ? new Set( opts.channels )\n : null\n\n // Mention-gating is either global (true) or scoped to named channels.\n const mentionEverywhere = opts.mentionOnly === true\n const mentionIn = Array.isArray( opts.mentionOnly ) && opts.mentionOnly.length\n ? new Set( opts.mentionOnly )\n : null\n\n const client = opts.client ?? await createDiscordClient()\n\n /** The most recently active allowed channel — last-resort proactive target. */\n let lastActiveChannelId: string | null = opts.homeChannelId ?? null\n\n // ── inbound: platform message → stimulus ──────────────────────────────────\n client.on('messageCreate', message => { void onMessage( message ) } )\n\n async function onMessage( message: DiscordLikeMessage ): Promise<void> {\n const self = client.user\n if( !self || message.author.id === self.id || message.author.bot ) return\n\n const isDM = !message.guildId\n if( !isDM && allowed && !allowed.has( message.channelId ) ) return\n\n const addressed = isDM || ( message.mentions?.has( self.id ) ?? false )\n // DMs are addressed by definition, so this never gates them.\n if( !addressed && ( mentionEverywhere || mentionIn?.has( message.channelId ) ) ) return\n\n const entityId = `discord:${ message.author.id }`\n const speaker = message.member?.displayName ?? message.author.displayName ?? message.author.username\n\n roster.record( {\n entityId,\n userId: message.author.id,\n ...( speaker ? { displayName: speaker } : {} ),\n ...( isDM ? { dmChannelId: message.channelId } : { lastChannelId: message.channelId } ),\n } )\n if( !isDM ) lastActiveChannelId = message.channelId\n\n // Being addressed is the one moment a presence cue is honest — the mind\n // may still choose silence, and typing expires on its own.\n if( addressed ) await message.channel.sendTyping?.().catch( () => {} )\n\n const said = ( message.cleanContent || message.content ).trim()\n const files = collectAttachments( message )\n\n // An attachment-only message used to die here on the empty body: no percept,\n // no log line, nothing. From the mind's side the person had simply gone\n // quiet — and Discord *makes* these, turning a long pasted markdown block\n // into a .md upload. Only a message with neither words nor files is nothing.\n if( !said && files.length === 0 ) return\n\n const shared = await renderAttachments(\n files, speaker,\n opts.readAttachments === false ? undefined : fetchAttachmentText,\n )\n const text = [ said, shared ].filter( Boolean ).join('\\n')\n\n await will.perceive( {\n text,\n from: entityId,\n thread: `discord:${ message.channelId }`,\n // `isDM` has been computed on every inbound since this bridge shipped and\n // used only to pick a roster field. It is the one fact that makes a room\n // the right or wrong place to say something, and the mind never saw it —\n // which is how a follow-up promised in a DM went out to #general.\n direct: isDM,\n ...( speaker ? { speaker } : {} ),\n } )\n }\n\n /**\n * Normalise whatever the client gave us into attachments.\n *\n * `.values()` first: a discord.js Collection is a Map, so `for..of` over it\n * yields `[id, attachment]` pairs and every field reads `undefined`. Arrays\n * expose `.values()` too and yield their elements, so one branch covers the\n * real client, a plain array, and a Map alike.\n */\n function collectAttachments( message: DiscordLikeMessage ): ChannelAttachment[] {\n if( !message.attachments ) return []\n const source = message.attachments as { values?: () => Iterable<DiscordLikeAttachment> }\n const items: Iterable<DiscordLikeAttachment> = typeof source.values === 'function'\n ? source.values()\n : message.attachments as Iterable<DiscordLikeAttachment>\n\n const out: ChannelAttachment[] = []\n for( const a of items )\n out.push( {\n name: a.name ?? 'unnamed',\n ...( a.contentType ? { contentType: a.contentType } : {} ),\n ...( a.size != null ? { size: a.size } : {} ),\n ...( a.url ? { url: a.url } : {} ),\n } )\n return out\n }\n\n /** Fetch one text attachment — Discord CDN only, size-capped. */\n async function fetchAttachmentText( a: ChannelAttachment ): Promise<string | null> {\n if( !a.url || !isTextual( a ) ) return null\n let host: string\n try { host = new URL( a.url ).hostname }\n catch { return null }\n if( !DISCORD_CDN_HOSTS.has( host ) ){\n log(`refusing to fetch attachment '${ a.name }' from non-CDN host ${ host }`)\n return null\n }\n if( a.size != null && a.size > MAX_FETCH_BYTES ){\n log(`attachment '${ a.name }' is ${ a.size } bytes — naming it without reading`)\n return null\n }\n const res = await fetch( a.url, { signal: AbortSignal.timeout( 10_000 ) } )\n if( !res.ok ){\n log(`attachment '${ a.name }' fetch failed: ${ res.status }`)\n return null\n }\n return ( await res.text() ).slice( 0, MAX_FETCH_BYTES )\n }\n\n // ── outbound: projected utterance → the addressee ─────────────────────────\n // The facade has no off(); the bridge gates its handler on `closed` instead.\n let closed = false\n will.on('message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )\n\n async function deliver( m: WillMessage ): Promise<void> {\n const peer = m.to ? roster.resolve( m.to ) : undefined\n const chunks = chunkText( m.content, DISCORD_MESSAGE_LIMIT )\n\n // A reply goes back to the room it was said in. `m.thread` is the thread from\n // the `perceive()` that prompted this — `discord:<channelId>` — so it is not a\n // guess about where this person usually is, it is where they just spoke.\n //\n // Everything below it IS a guess, and the guesses were wrong in the way that\n // matters most: a DM arrived, she answered it in seconds, and the answer went\n // to the shared server channel because `lastChannelId` still held the last\n // room they had been in together. She looked like she was ignoring him.\n //\n // Unprompted utterances carry no thread — nothing was said to them — so those\n // still fall through to the roster, which is the right behaviour there.\n const replyTo = m.thread?.startsWith('discord:') ? m.thread.slice('discord:'.length ) : undefined\n\n // Preference order: the room they spoke in → where we last shared a room →\n // their DM → home channel.\n const channelIds = [ replyTo, peer?.lastChannelId, peer?.dmChannelId, opts.homeChannelId ?? undefined, lastActiveChannelId ?? undefined ]\n for( const id of channelIds ){\n if( !id ) continue\n try {\n const channel = await client.channels.fetch( id ) as DiscordLikeChannel | null\n if( !channel?.send ) continue\n for( const chunk of chunks ) await channel.send( chunk )\n return\n }\n catch { /* try the next route */ }\n }\n if( peer ){\n try {\n const user = await client.users.fetch( peer.userId )\n for( const chunk of chunks ) await user.send( chunk )\n return\n }\n catch { /* fall through */ }\n }\n log(`no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)`)\n }\n\n // ── lifecycle ──────────────────────────────────────────────────────────────\n const bridge: ChannelBridge = {\n kind: 'discord',\n async start(): Promise<void> {\n if( !client.user ){\n // discord.js ≥14.22 renamed `ready` → `clientReady`. Subscribing to the\n // old name is what triggers its DeprecationWarning, so we take the new\n // name and poll `isReady()` for older builds rather than listening.\n const ready = new Promise<void>( resolve => {\n let poll: ReturnType<typeof setInterval> | null = null\n const done = (): void => { if( poll ) clearInterval( poll ); resolve() }\n client.once('clientReady', done )\n poll = setInterval( () => { if( client.isReady?.() ) done() }, 100 )\n poll.unref?.()\n } )\n await client.login( opts.token ?? '')\n await ready\n }\n log(`${ will.name } is present on Discord as user ${ client.user?.id }`)\n },\n async close(): Promise<void> {\n if( closed ) return\n closed = true\n roster.flush()\n await Promise.resolve( client.destroy() ).catch( () => {} )\n },\n }\n return bridge\n}\n\n/** Build a real discord.js client (lazy import keeps it out of non-Discord hosts). */\nasync function createDiscordClient(): Promise<DiscordLikeClient> {\n let mod: typeof import('discord.js')\n try { mod = await import('discord.js') }\n catch {\n throw new Error('discord.js is not installed (it is an optionalDependency) — run `bun add discord.js` / `npm i discord.js` and retry.')\n }\n const { Client, GatewayIntentBits, Partials } = mod\n return new Client( {\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [ Partials.Channel ], // DMs arrive on uncached channels\n } ) as unknown as DiscordLikeClient\n}\n\n/**\n * Parse `WILL_DISCORD_MENTION_ONLY` into the `mentionOnly` option.\n *\n * Accepts a boolean OR a channel list, because \"only speak when spoken to\" is\n * rarely a whole-server property — it is how you stay present in a busy channel\n * without narrating in it.\n *\n * `1` / `true` / `yes` → true (everywhere)\n * `0` / `false` / unset → false (nowhere)\n * `123,456` → only those channels\n *\n * Exported so every host parses it identically; the CLI and any SDK host share\n * this rather than each re-deriving the syntax.\n */\nexport function parseMentionOnly( raw?: string ): boolean | string[] {\n const v = raw?.trim()\n if( !v ) return false\n if( /^(1|true|yes)$/i.test( v ) ) return true\n if( /^(0|false|no)$/i.test( v ) ) return false\n\n const ids = v.split(',').map( s => s.trim() ).filter( Boolean )\n return ids.length ? ids : false\n}\n\n/**\n * Parse `WILL_DISCORD_CHANNELS`. `*` (or unset/empty) means every channel the Will\n * can see — being added to a channel in Discord is then all it takes. Anything else\n * restrains it to exactly the ids listed.\n */\nexport function parseChannels( raw?: string ): string[] | undefined {\n const ids = raw?.split(',').map( s => s.trim() ).filter( Boolean )\n return ids?.length ? ids : undefined\n}\n"]}
|
|
@@ -125,6 +125,8 @@ async function connectWhatsApp(will, opts = {}) {
|
|
|
125
125
|
text,
|
|
126
126
|
from: entityId,
|
|
127
127
|
thread: `whatsapp:${jid}`,
|
|
128
|
+
// A WhatsApp group jid ends in `@g.us`; anything else is a one-to-one chat.
|
|
129
|
+
direct: !jid.endsWith("@g.us"),
|
|
128
130
|
...speaker ? { speaker } : {}
|
|
129
131
|
});
|
|
130
132
|
}
|
|
@@ -134,8 +136,9 @@ async function connectWhatsApp(will, opts = {}) {
|
|
|
134
136
|
async function deliver(m) {
|
|
135
137
|
const peer = m.to ? roster.resolve(m.to) : void 0;
|
|
136
138
|
const chunks = chunkText(m.content, WHATSAPP_MESSAGE_LIMIT);
|
|
139
|
+
const replyTo = m.thread?.startsWith("whatsapp:") ? m.thread.slice("whatsapp:".length) : void 0;
|
|
137
140
|
const derivedDm = m.to?.startsWith("whatsapp:") ? dmJidFor(m.to.slice("whatsapp:".length)) : void 0;
|
|
138
|
-
const targets = [peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? void 0, lastActiveChatId ?? void 0];
|
|
141
|
+
const targets = [replyTo, peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? void 0, lastActiveChatId ?? void 0];
|
|
139
142
|
for (const jid of targets) {
|
|
140
143
|
if (!jid) continue;
|
|
141
144
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/channels/roster.ts","../../src/channels/types.ts","../../src/channels/whatsapp.ts"],"names":["state"],"mappings":"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","file":"whatsapp.js","sourcesContent":["// ─────────────────────────────────────────────────────────────\n// src/channels/roster.ts — who the Will knows on a platform, and where\n// ─────────────────────────────────────────────────────────────\n//\n// The mind knows *entities*; a platform knows user ids and channels. The roster\n// is the durable seam between them: for each entity the Will has met on a\n// channel it records how to reach them again — so a *proactive* utterance\n// (`message.to` from the mind's own initiative) can find its person after a\n// restart, not just within one session.\n//\n// It persists as a small JSON file next to the PMA artifact. Writes are\n// throttled (the file is advisory routing state, not cognition — losing the\n// last few seconds costs a fallback delivery, never memory).\n// ─────────────────────────────────────────────────────────────\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/** How to reach one entity on the platform. */\nexport interface RosterEntry {\n /** The mind-side entity id, e.g. 'discord:80351110224678912'. */\n entityId: string\n /** The platform-side user id. */\n userId: string\n /** Last display name seen (advisory — the *learned* name lives in the mind). */\n displayName?: string\n /** DM channel id, once one is known. */\n dmChannelId?: string\n /** Last shared (guild) channel this entity spoke in. */\n lastChannelId?: string\n /** Epoch ms of the last message seen from them. */\n lastSeenAt: number\n}\n\nconst FLUSH_MS = 2_000\n\nexport class ChannelRoster {\n private entries = new Map<string, RosterEntry>()\n private dirty = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor( private readonly path: string ) {\n if( existsSync( path ) ){\n try {\n const raw = JSON.parse( readFileSync( path, 'utf8') ) as RosterEntry[]\n for( const e of Array.isArray( raw ) ? raw : [] ) this.entries.set( e.entityId, e )\n }\n catch { /* a corrupt roster is not worth failing a boot over — start fresh */ }\n }\n }\n\n /** Upsert what we just learned about an entity; schedules a throttled flush. */\n record( update: { entityId: string; userId: string } & Partial<Omit<RosterEntry, 'entityId' | 'userId'>> ): RosterEntry {\n const prev = this.entries.get( update.entityId )\n const next: RosterEntry = {\n lastSeenAt: Date.now(),\n ...prev,\n ...Object.fromEntries( Object.entries( update ).filter( ( [ , v ] ) => v !== undefined ) ) as typeof update,\n }\n this.entries.set( next.entityId, next )\n this.dirty = true\n if( !this.timer ){\n this.timer = setTimeout( () => { this.timer = null; this.flush() }, FLUSH_MS )\n this.timer.unref?.()\n }\n return next\n }\n\n resolve( entityId: string ): RosterEntry | undefined {\n return this.entries.get( entityId )\n }\n\n all(): RosterEntry[] {\n return [ ...this.entries.values() ]\n }\n\n /** Write to disk now (no-op when clean). Called by bridges on close. */\n flush(): void {\n if( !this.dirty ) return\n try {\n mkdirSync( dirname( this.path ), { recursive: true } )\n writeFileSync( this.path, JSON.stringify( this.all(), null, 2 ) )\n this.dirty = false\n }\n catch { /* advisory state — never take the mind down over it */ }\n }\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/types.ts — the channel-bridge contract\n// ─────────────────────────────────────────────────────────────\n//\n// A channel bridge puts a Will *in a place where people already are* (Discord,\n// Telegram, Slack, …). It is a host surface, not a cognition surface: it turns\n// platform messages into `perceive` stimuli and delivers the Will's projected\n// utterances back — nothing more. The paradigm survives the crossing:\n//\n// • every platform user is an entity the Will comes to know (`from`),\n// with a *learned* name (`speaker`) — never a placeholder;\n// • every platform channel/DM is a conversation thread (`thread`);\n// • the Will decides when to speak. Silence is a valid outcome, so a\n// bridge never fabricates a reply and never times a message out into\n// an error.\n//\n// Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):\n// they wrap the SDK facade, not the stem.\n// ─────────────────────────────────────────────────────────────\n\n/** A running connection between one Will and one platform. */\nexport interface ChannelBridge {\n /** Platform kind, e.g. 'discord'. */\n readonly kind: string\n /** Connect and start relaying. Resolves once the bridge is live. */\n start(): Promise<void>\n /** Disconnect and release resources. Idempotent. */\n close(): Promise<void>\n}\n\n/** Split a message into platform-sized chunks on natural boundaries. */\nexport function chunkText( text: string, max: number ): string[] {\n if( text.length <= max ) return [ text ]\n const chunks: string[] = []\n let rest = text\n while( rest.length > max ){\n // Prefer a paragraph break, then a line break, then a space — else hard-cut.\n const window = rest.slice( 0, max )\n const cut = Math.max( window.lastIndexOf('\\n\\n'), window.lastIndexOf('\\n'), window.lastIndexOf(' ') )\n const at = cut > max * 0.5 ? cut : max\n chunks.push( rest.slice( 0, at ).trimEnd() )\n rest = rest.slice( at ).trimStart()\n }\n if( rest ) chunks.push( rest )\n return chunks\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/whatsapp.ts — a Will present on WhatsApp\n// ─────────────────────────────────────────────────────────────\n//\n// Same paradigm as the Discord bridge, different room:\n//\n// inbound DM / group message → will.perceive({ from, speaker, text, thread })\n// — every author is `whatsapp:<number>` (stable across chats), the\n// push name is *learned* by the mind, and each chat (DM or group)\n// is its own conversation thread.\n// outbound will.on('message') → the addressee's last shared group, else\n// their DM (a WhatsApp DM jid is derivable from the number — no\n// roster miss is fatal), else the home chat.\n//\n// TRANSPORT — read before deploying. This bridge speaks the *linked-device*\n// protocol via Baileys: the Will pairs to a WhatsApp account by QR scan, like\n// WhatsApp Web. That is not a sanctioned bot API — WhatsApp's terms don't\n// allow automation on personal accounts, and accounts doing it can be banned.\n// Meta's sanctioned path (the Business Cloud API) needs a business account,\n// webhook hosting, and app review — no 2-minute pairing. Operator's choice;\n// docs/channels/whatsapp.md is explicit about the trade. Use a spare number.\n//\n// Baileys is imported lazily inside `createWhatsAppSocket` — tests (and hosts\n// that bring their own transport) inject `socket`, and the structural\n// `WaLikeSocket` type keeps the dependency out of the type graph.\n// ─────────────────────────────────────────────────────────────\n\nimport type { Will, WillMessage } from '#sdk/will'\nimport { ChannelRoster } from '#channels/roster'\nimport { chunkText, type ChannelBridge } from '#channels/types'\n\n/** WhatsApp's hard per-message cap. */\nconst WHATSAPP_MESSAGE_LIMIT = 65_536\n\n// ── The slice of a Baileys socket the bridge actually uses (structural) ──────\n\nexport interface WaLikeMessage {\n key: {\n remoteJid?: string | null\n fromMe?: boolean | null\n /** Sender jid inside a group (absent in DMs). */\n participant?: string | null\n }\n /** Sender's display (\"push\") name. */\n pushName?: string | null\n messageStubType?: number | null\n message?: {\n conversation?: string | null\n extendedTextMessage?: { text?: string | null; contextInfo?: { mentionedJid?: string[] | null } | null } | null\n imageMessage?: { caption?: string | null } | null\n videoMessage?: { caption?: string | null } | null\n } | null\n}\n\nexport interface WaLikeSocket {\n /** The paired account, once connected. id like '4915…:12@s.whatsapp.net'. */\n user?: { id: string; name?: string } | null\n ev: { on( event: 'messages.upsert', fn: ( u: { messages: WaLikeMessage[]; type?: string } ) => void ): unknown }\n sendMessage( jid: string, content: { text: string } ): Promise<unknown>\n sendPresenceUpdate?( state: 'composing' | 'paused', jid?: string ): Promise<unknown>\n /** Tear the connection down (does not unlink the device). */\n end?( err?: Error ): void\n}\n\n// ── Options ──────────────────────────────────────────────────────────────────\n\nexport interface WhatsAppBridgeOptions {\n /** Chat jids the Will inhabits (groups `…@g.us`, DMs `…@s.whatsapp.net`). Unset = every chat. */\n chats?: string[]\n /** Perceive group messages only when the Will is @mentioned (DMs always perceived). */\n mentionOnly?: boolean\n /** Fallback chat jid for utterances with no reachable addressee. */\n homeChatId?: string\n /** Linked-device credentials dir (default: ./.will/<willId>.wa-auth). QR pairs on first run. */\n authPath?: string\n /** Roster path (default: ./.will/<willId>.whatsapp.json). */\n rosterPath?: string\n /** Test / power-user seam: bring your own socket; Baileys is never imported. */\n socket?: WaLikeSocket\n log?: ( msg: string ) => void\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nconst isGroupJid = ( jid: string ): boolean => jid.endsWith('@g.us')\n\n/** '4915123:7@s.whatsapp.net' → '4915123' (device + server stripped). */\nconst bareId = ( jid: string ): string => jid.split('@')[0]!.split(':')[0]!\n\n/** A DM jid is derivable from the bare number — WhatsApp's gift to proactivity. */\nconst dmJidFor = ( userId: string ): string => `${ userId }@s.whatsapp.net`\n\nfunction textOf( m: WaLikeMessage ): string {\n const msg = m.message\n return msg?.conversation\n ?? msg?.extendedTextMessage?.text\n ?? msg?.imageMessage?.caption\n ?? msg?.videoMessage?.caption\n ?? ''\n}\n\n// ── The bridge ───────────────────────────────────────────────────────────────\n\n/**\n * Connect a Will to WhatsApp over the linked-device protocol. First run prints\n * a QR to pair (Settings → Linked devices → Link a device); credentials persist\n * in `authPath` so later runs reconnect silently. Close via the returned\n * `ChannelBridge.close()` — the Will keeps ticking; it only loses this surface.\n */\nexport async function connectWhatsApp( will: Will, opts: WhatsAppBridgeOptions = {} ): Promise<ChannelBridge> {\n const log = opts.log ?? ( ( m: string ) => console.error(`[will:whatsapp] ${ m }`) )\n const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.whatsapp.json`)\n const allowed = opts.chats?.length ? new Set( opts.chats ) : null\n\n let closed = false\n const socket = opts.socket ?? await createWhatsAppSocket( {\n authPath: opts.authPath ?? `.will/${ will.id }.wa-auth`,\n log,\n stillOpen: () => !closed,\n } )\n\n /** The most recently active allowed chat — last-resort proactive target. */\n let lastActiveChatId: string | null = opts.homeChatId ?? null\n\n // ── inbound: platform message → stimulus ──────────────────────────────────\n socket.ev.on('messages.upsert', ( { messages, type } ) => {\n if( type && type !== 'notify') return // history sync/corrections are not new percepts\n for( const m of messages ) void onMessage( m )\n } )\n\n async function onMessage( m: WaLikeMessage ): Promise<void> {\n const jid = m.key.remoteJid\n if( !jid || m.key.fromMe || !m.message || m.messageStubType ) return\n if( jid.endsWith('@broadcast') || jid.endsWith('@newsletter') ) return // stories/broadcasts aren't a room the Will is in\n if( allowed && !allowed.has( jid ) ) return\n\n const isGroup = isGroupJid( jid )\n const senderJid = isGroup ? m.key.participant : jid\n if( !senderJid ) return\n\n const selfId = socket.user ? bareId( socket.user.id ) : null\n const mentioned = m.message.extendedTextMessage?.contextInfo?.mentionedJid ?? []\n const addressed = !isGroup || ( selfId != null && mentioned.some( j => bareId( j ) === selfId ) )\n if( opts.mentionOnly && !addressed ) return\n\n const userId = bareId( senderJid )\n const entityId = `whatsapp:${ userId }`\n const speaker = m.pushName ?? undefined\n\n roster.record( {\n entityId,\n userId,\n ...( speaker ? { displayName: speaker } : {} ),\n ...( isGroup ? { lastChannelId: jid } : { dmChannelId: jid } ),\n } )\n if( isGroup ) lastActiveChatId = jid\n\n // Same rule as Discord: a presence cue only when addressed — the mind may\n // still choose silence, and 'composing' expires on its own.\n if( addressed ) await socket.sendPresenceUpdate?.('composing', jid ).catch( () => {} )\n\n const text = textOf( m )\n if( !text.trim() ) return\n\n await will.perceive( {\n text,\n from: entityId,\n thread: `whatsapp:${ jid }`,\n ...( speaker ? { speaker } : {} ),\n } )\n }\n\n // ── outbound: projected utterance → the addressee ─────────────────────────\n will.on('message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )\n\n async function deliver( m: WillMessage ): Promise<void> {\n const peer = m.to ? roster.resolve( m.to ) : undefined\n const chunks = chunkText( m.content, WHATSAPP_MESSAGE_LIMIT )\n\n // Last shared group → known DM → DM derived from the entity id itself →\n // home chat → last active chat. Unlike Discord, an unmet-but-addressed\n // entity is still reachable: `whatsapp:<number>` implies its DM jid.\n const derivedDm = m.to?.startsWith('whatsapp:') ? dmJidFor( m.to.slice('whatsapp:'.length ) ) : undefined\n const targets = [ peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? undefined, lastActiveChatId ?? undefined ]\n for( const jid of targets ){\n if( !jid ) continue\n try {\n for( const chunk of chunks ) await socket.sendMessage( jid, { text: chunk } )\n return\n }\n catch { /* try the next route */ }\n }\n log(`no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)`)\n }\n\n // ── lifecycle ──────────────────────────────────────────────────────────────\n return {\n kind: 'whatsapp',\n async start(): Promise<void> {\n // createWhatsAppSocket resolves already-open; an injected socket is the\n // caller's to have readied.\n log(`${ will.name } is present on WhatsApp${ socket.user ? ` as ${ bareId( socket.user.id ) }` : '' }`)\n },\n async close(): Promise<void> {\n if( closed ) return\n closed = true\n roster.flush()\n try { socket.end?.() } catch { /* already down */ }\n },\n }\n}\n\n// ── The real transport (Baileys, lazily imported) ────────────────────────────\n\ninterface SocketFactoryOpts {\n authPath: string\n log: ( msg: string ) => void\n /** Reconnect only while the bridge is open. */\n stillOpen: () => boolean\n}\n\n/**\n * A reconnect-stable facade over Baileys. Baileys' own pattern replaces the\n * socket object on every reconnect; the facade keeps one stable identity the\n * bridge can hold, re-attaching its subscribers to each inner incarnation.\n * Resolves once the first connection is open (after QR pairing on first run).\n */\nasync function createWhatsAppSocket( o: SocketFactoryOpts ): Promise<WaLikeSocket> {\n let baileys: typeof import('baileys')\n try { baileys = await import('baileys') }\n catch {\n throw new Error('baileys is not installed (it is an optionalDependency) — run `bun add baileys` / `npm i baileys` and retry.')\n }\n const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = baileys\n\n // Baileys logs through pino at info by default — JSON noise all over the\n // host's stderr. A minimal pino-shaped silent logger (child() returns\n // itself, every level a no-op) is what it actually needs; hand-rolling it\n // beats importing a transitive dep that may not resolve from dist/.\n const noop = (): void => {}\n const logger = {\n level: 'silent',\n child(){ return logger },\n trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop,\n }\n\n type Handler = ( u: { messages: WaLikeMessage[]; type?: string } ) => void\n const subscribers: Handler[] = []\n let inner: ReturnType<typeof makeWASocket> | null = null\n\n const facade: WaLikeSocket = {\n get user(){ return inner?.user ? { id: inner.user.id, name: inner.user.name ?? undefined } : null },\n ev: { on: ( _e, fn ) => { subscribers.push( fn ) } },\n sendMessage: ( jid, content ) => {\n if( !inner ) return Promise.reject( new Error('whatsapp socket not connected') )\n return inner.sendMessage( jid, content )\n },\n sendPresenceUpdate: ( state, jid ) => inner?.sendPresenceUpdate( state, jid ) ?? Promise.resolve(),\n end: () => inner?.end( undefined ),\n }\n\n const { state, saveCreds } = await useMultiFileAuthState( o.authPath )\n\n await new Promise<void>( ( resolveOpen, rejectOpen ) => {\n let opened = false\n\n function connect(): void {\n const sock = makeWASocket( { auth: state, logger: logger as never } )\n inner = sock\n\n sock.ev.on('creds.update', saveCreds )\n sock.ev.on('messages.upsert', u => { for( const fn of subscribers ) fn( u as never ) } )\n\n sock.ev.on('connection.update', update => {\n const { connection, lastDisconnect, qr } = update as {\n connection?: string; lastDisconnect?: { error?: unknown }; qr?: string\n }\n\n if( qr ) void printQr( qr, o.log )\n\n if( connection === 'open' && !opened ){\n opened = true\n resolveOpen()\n }\n if( connection === 'close'){\n const code = ( lastDisconnect?.error as { output?: { statusCode?: number } } | undefined )?.output?.statusCode\n if( code === DisconnectReason.loggedOut ){\n const err = new Error('WhatsApp unlinked this device (logged out) — delete the auth dir and pair again.')\n o.log( err.message )\n if( !opened ) rejectOpen( err )\n return\n }\n if( o.stillOpen() ){\n o.log(`connection closed (status ${ code ?? '?' }) — reconnecting…`)\n setTimeout( connect, 3_000 )\n }\n }\n } )\n }\n\n connect()\n } )\n\n return facade\n}\n\n/** QR to the terminal; falls back to the raw pairing string if the tiny renderer is absent. */\nasync function printQr( qr: string, log: ( m: string ) => void ): Promise<void> {\n log('pair this device: WhatsApp → Settings → Linked devices → Link a device')\n try {\n type QrModule = { generate( s: string, o: { small: boolean } ): void }\n const qrt = ( await import('qrcode-terminal' as string ) ) as QrModule & { default?: QrModule }\n ;( qrt.default ?? qrt ).generate( qr, { small: true } )\n }\n catch {\n log(`qrcode-terminal not installed — raw pairing code:\\n${ qr }`)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/channels/roster.ts","../../src/channels/types.ts","../../src/channels/whatsapp.ts"],"names":["state"],"mappings":";;;;AAkCA,IAAM,QAAA,GAAW,GAAA;AAEV,IAAM,gBAAN,MAAoB;AAAA,EAKzB,YAA8B,IAAA,EAAe;AAAf,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAC5B,IAAA,IAAI,UAAA,CAAY,IAAK,CAAA,EAAG;AACtB,MAAA,IAAI;AACF,QAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAO,YAAA,CAAc,IAAA,EAAM,MAAM,CAAE,CAAA;AACpD,QAAA,KAAA,MAAW,CAAA,IAAK,KAAA,CAAM,OAAA,CAAS,GAAI,CAAA,GAAI,GAAA,GAAM,EAAC,EAAI,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAK,CAAA,CAAE,UAAU,CAAE,CAAA;AAAA,MACpF,CAAA,CAAA,MACM;AAAA,MAAwE;AAAA,IAChF;AAAA,EACF;AAAA,EAR8B,IAAA;AAAA,EAJtB,OAAA,uBAAc,GAAA,EAAyB;AAAA,EACvC,KAAA,GAAQ,KAAA;AAAA,EACR,KAAA,GAA8C,IAAA;AAAA;AAAA,EAatD,OAAQ,MAAA,EAAgH;AACtH,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAK,OAAO,QAAS,CAAA;AAC/C,IAAA,MAAM,IAAA,GAAoB;AAAA,MACxB,UAAA,EAAY,KAAK,GAAA,EAAI;AAAA,MACrB,GAAG,IAAA;AAAA,MACH,GAAG,MAAA,CAAO,WAAA,CAAa,MAAA,CAAO,QAAS,MAAO,CAAA,CAAE,MAAA,CAAQ,CAAE,GAAI,CAAE,CAAA,KAAO,CAAA,KAAM,MAAU,CAAE;AAAA,KAC3F;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAK,IAAA,CAAK,QAAA,EAAU,IAAK,CAAA;AACtC,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,IAAA,IAAI,CAAC,KAAK,KAAA,EAAO;AACf,MAAA,IAAA,CAAK,KAAA,GAAQ,WAAY,MAAM;AAAE,QAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AAAM,QAAA,IAAA,CAAK,KAAA,EAAM;AAAA,MAAE,GAAG,QAAS,CAAA;AAC7E,MAAA,IAAA,CAAK,MAAM,KAAA,IAAQ;AAAA,IACrB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,QAAS,QAAA,EAA4C;AACnD,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAK,QAAS,CAAA;AAAA,EACpC;AAAA,EAEA,GAAA,GAAqB;AACnB,IAAA,OAAO,CAAE,GAAG,IAAA,CAAK,OAAA,CAAQ,QAAS,CAAA;AAAA,EACpC;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,CAAC,KAAK,KAAA,EAAQ;AAClB,IAAA,IAAI;AACF,MAAA,SAAA,CAAW,QAAS,IAAA,CAAK,IAAK,GAAG,EAAE,SAAA,EAAW,MAAO,CAAA;AACrD,MAAA,aAAA,CAAe,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAW,KAAK,GAAA,EAAI,EAAG,IAAA,EAAM,CAAE,CAAE,CAAA;AAChE,MAAA,IAAA,CAAK,KAAA,GAAQ,KAAA;AAAA,IACf,CAAA,CAAA,MACM;AAAA,IAA0D;AAAA,EAClE;AACF,CAAA;;;ACmCO,SAAS,SAAA,CAAW,MAAc,GAAA,EAAwB;AAC/D,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,GAAA,EAAM,OAAO,CAAE,IAAK,CAAA;AACvC,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,IAAI,IAAA,GAAO,IAAA;AACX,EAAA,OAAO,IAAA,CAAK,SAAS,GAAA,EAAK;AAExB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAO,CAAA,EAAG,GAAI,CAAA;AAClC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAK,MAAA,CAAO,YAAY,MAAM,CAAA,EAAG,MAAA,CAAO,WAAA,CAAY,IAAI,CAAA,EAAG,MAAA,CAAO,WAAA,CAAY,GAAG,CAAE,CAAA;AACpG,IAAA,MAAM,EAAA,GAAK,GAAA,GAAM,GAAA,GAAM,GAAA,GAAM,GAAA,GAAM,GAAA;AACnC,IAAA,MAAA,CAAO,KAAM,IAAA,CAAK,KAAA,CAAO,GAAG,EAAG,CAAA,CAAE,SAAU,CAAA;AAC3C,IAAA,IAAA,GAAO,IAAA,CAAK,KAAA,CAAO,EAAG,CAAA,CAAE,SAAA,EAAU;AAAA,EACpC;AACA,EAAA,IAAI,IAAA,EAAO,MAAA,CAAO,IAAA,CAAM,IAAK,CAAA;AAC7B,EAAA,OAAO,MAAA;AACT;;;ACvGA,IAAM,sBAAA,GAAyB,KAAA;AAoD/B,IAAM,UAAA,GAAa,CAAE,GAAA,KAA0B,GAAA,CAAI,SAAS,OAAO,CAAA;AAGnE,IAAM,MAAA,GAAS,CAAE,GAAA,KAAyB,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA,CAAG,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAA;AAGzE,IAAM,QAAA,GAAW,CAAE,MAAA,KAA4B,CAAA,EAAI,MAAO,CAAA,eAAA,CAAA;AAE1D,SAAS,OAAQ,CAAA,EAA2B;AAC1C,EAAA,MAAM,MAAM,CAAA,CAAE,OAAA;AACd,EAAA,OAAO,GAAA,EAAK,YAAA,IACL,GAAA,EAAK,mBAAA,EAAqB,IAAA,IAC1B,KAAK,YAAA,EAAc,OAAA,IACnB,GAAA,EAAK,YAAA,EAAc,OAAA,IACnB,EAAA;AACT;AAUA,eAAsB,eAAA,CAAiB,IAAA,EAAY,IAAA,GAA8B,EAAC,EAA4B;AAC5G,EAAA,MAAM,GAAA,GAAU,KAAK,GAAA,KAAS,CAAE,MAAe,OAAA,CAAQ,KAAA,CAAM,CAAA,gBAAA,EAAoB,CAAE,CAAA,CAAE,CAAA,CAAA;AACrF,EAAA,MAAM,MAAA,GAAU,IAAI,aAAA,CAAe,IAAA,CAAK,cAAc,CAAA,MAAA,EAAU,IAAA,CAAK,EAAG,CAAA,cAAA,CAAgB,CAAA;AACxF,EAAA,MAAM,OAAA,GAAU,KAAK,KAAA,EAAO,MAAA,GAAS,IAAI,GAAA,CAAK,IAAA,CAAK,KAAM,CAAA,GAAI,IAAA;AAE7D,EAAA,IAAI,MAAA,GAAS,KAAA;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,IAAU,MAAM,oBAAA,CAAsB;AAAA,IACxD,QAAA,EAAU,IAAA,CAAK,QAAA,IAAY,CAAA,MAAA,EAAU,KAAK,EAAG,CAAA,QAAA,CAAA;AAAA,IAC7C,GAAA;AAAA,IACA,SAAA,EAAW,MAAM,CAAC;AAAA,GAClB,CAAA;AAGF,EAAA,IAAI,gBAAA,GAAkC,KAAK,UAAA,IAAc,IAAA;AAGzD,EAAA,MAAA,CAAO,GAAG,EAAA,CAAG,iBAAA,EAAmB,CAAE,EAAE,QAAA,EAAU,MAAK,KAAO;AACxD,IAAA,IAAI,IAAA,IAAQ,SAAS,QAAA,EAAU;AAC/B,IAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAW,KAAK,SAAA,CAAW,CAAE,CAAA;AAAA,EAC/C,CAAE,CAAA;AAEF,EAAA,eAAe,UAAW,CAAA,EAAkC;AAC1D,IAAA,MAAM,GAAA,GAAM,EAAE,GAAA,CAAI,SAAA;AAClB,IAAA,IAAI,CAAC,OAAO,CAAA,CAAE,GAAA,CAAI,UAAU,CAAC,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,eAAA,EAAkB;AAC9D,IAAA,IAAI,IAAI,QAAA,CAAS,YAAY,KAAK,GAAA,CAAI,QAAA,CAAS,aAAa,CAAA,EAAI;AAChE,IAAA,IAAI,OAAA,IAAW,CAAC,OAAA,CAAQ,GAAA,CAAK,GAAI,CAAA,EAAI;AAErC,IAAA,MAAM,OAAA,GAAY,WAAY,GAAI,CAAA;AAClC,IAAA,MAAM,SAAA,GAAY,OAAA,GAAU,CAAA,CAAE,GAAA,CAAI,WAAA,GAAc,GAAA;AAChD,IAAA,IAAI,CAAC,SAAA,EAAY;AAEjB,IAAA,MAAM,SAAY,MAAA,CAAO,IAAA,GAAO,OAAQ,MAAA,CAAO,IAAA,CAAK,EAAG,CAAA,GAAI,IAAA;AAC3D,IAAA,MAAM,YAAY,CAAA,CAAE,OAAA,CAAQ,mBAAA,EAAqB,WAAA,EAAa,gBAAgB,EAAC;AAC/E,IAAA,MAAM,SAAA,GAAY,CAAC,OAAA,IAAa,MAAA,IAAU,IAAA,IAAQ,SAAA,CAAU,IAAA,CAAM,CAAA,CAAA,KAAK,MAAA,CAAQ,CAAE,CAAA,KAAM,MAAO,CAAA;AAC9F,IAAA,IAAI,IAAA,CAAK,WAAA,IAAe,CAAC,SAAA,EAAY;AAErC,IAAA,MAAM,MAAA,GAAW,OAAQ,SAAU,CAAA;AACnC,IAAA,MAAM,QAAA,GAAW,YAAa,MAAO,CAAA,CAAA;AACrC,IAAA,MAAM,OAAA,GAAW,EAAE,QAAA,IAAY,MAAA;AAE/B,IAAA,MAAA,CAAO,MAAA,CAAQ;AAAA,MACb,QAAA;AAAA,MACA,MAAA;AAAA,MACA,GAAK,OAAA,GAAU,EAAE,WAAA,EAAa,OAAA,KAAY,EAAC;AAAA,MAC3C,GAAK,UAAU,EAAE,aAAA,EAAe,KAAI,GAAI,EAAE,aAAa,GAAA;AAAI,KAC3D,CAAA;AACF,IAAA,IAAI,SAAU,gBAAA,GAAmB,GAAA;AAIjC,IAAA,IAAI,SAAA,QAAkB,MAAA,CAAO,kBAAA,GAAqB,aAAa,GAAI,CAAA,CAAE,MAAO,MAAM;AAAA,IAAC,CAAE,CAAA;AAErF,IAAA,MAAM,IAAA,GAAO,OAAQ,CAAE,CAAA;AACvB,IAAA,IAAI,CAAC,IAAA,CAAK,IAAA,EAAK,EAAI;AAEnB,IAAA,MAAM,KAAK,QAAA,CAAU;AAAA,MACnB,IAAA;AAAA,MACA,IAAA,EAAQ,QAAA;AAAA,MACR,MAAA,EAAQ,YAAa,GAAI,CAAA,CAAA;AAAA;AAAA,MAEzB,MAAA,EAAQ,CAAC,GAAA,CAAI,QAAA,CAAS,OAAO,CAAA;AAAA,MAC7B,GAAK,OAAA,GAAU,EAAE,OAAA,KAAY;AAAC,KAC9B,CAAA;AAAA,EACJ;AAGA,EAAA,IAAA,CAAK,EAAA,CAAG,SAAA,EAAW,CAAE,CAAA,KAAoB;AAAE,IAAA,IAAI,CAAC,MAAA,EAAS,KAAK,OAAA,CAAS,CAAE,CAAA;AAAA,EAAE,CAAE,CAAA;AAE7E,EAAA,eAAe,QAAS,CAAA,EAAgC;AACtD,IAAA,MAAM,OAAS,CAAA,CAAE,EAAA,GAAK,OAAO,OAAA,CAAS,CAAA,CAAE,EAAG,CAAA,GAAI,MAAA;AAC/C,IAAA,MAAM,MAAA,GAAS,SAAA,CAAW,CAAA,CAAE,OAAA,EAAS,sBAAuB,CAAA;AAW5D,IAAA,MAAM,OAAA,GAAY,CAAA,CAAE,MAAA,EAAQ,UAAA,CAAW,WAAW,CAAA,GAAI,CAAA,CAAE,MAAA,CAAO,KAAA,CAAM,WAAA,CAAY,MAAO,CAAA,GAAI,MAAA;AAC5F,IAAA,MAAM,SAAA,GAAY,CAAA,CAAE,EAAA,EAAI,UAAA,CAAW,WAAW,CAAA,GAAI,QAAA,CAAU,CAAA,CAAE,EAAA,CAAG,KAAA,CAAM,WAAA,CAAY,MAAO,CAAE,CAAA,GAAI,MAAA;AAChG,IAAA,MAAM,OAAA,GAAU,CAAE,OAAA,EAAS,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,WAAA,EAAa,SAAA,EAAW,IAAA,CAAK,UAAA,IAAc,MAAA,EAAW,gBAAA,IAAoB,MAAU,CAAA;AAC1I,IAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AACzB,MAAA,IAAI,CAAC,GAAA,EAAM;AACX,MAAA,IAAI;AACF,QAAA,KAAA,MAAW,KAAA,IAAS,QAAS,MAAM,MAAA,CAAO,YAAa,GAAA,EAAK,EAAE,IAAA,EAAM,KAAA,EAAQ,CAAA;AAC5E,QAAA;AAAA,MACF,CAAA,CAAA,MACM;AAAA,MAA2B;AAAA,IACnC;AACA,IAAA,GAAA,CAAI,8BAA+B,CAAA,CAAE,EAAG,qBAAiB,CAAA,CAAE,OAAA,CAAQ,MAAO,CAAA,OAAA,CAAS,CAAA;AAAA,EACrF;AAGA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,UAAA;AAAA,IACN,MAAM,KAAA,GAAuB;AAG3B,MAAA,GAAA,CAAI,CAAA,EAAI,IAAA,CAAK,IAAK,CAAA,uBAAA,EAA2B,OAAO,IAAA,GAAO,CAAA,IAAA,EAAQ,MAAA,CAAQ,MAAA,CAAO,IAAA,CAAK,EAAG,CAAE,CAAA,CAAA,GAAK,EAAG,CAAA,CAAE,CAAA;AAAA,IACxG,CAAA;AAAA,IACA,MAAM,KAAA,GAAuB;AAC3B,MAAA,IAAI,MAAA,EAAS;AACb,MAAA,MAAA,GAAS,IAAA;AACT,MAAA,MAAA,CAAO,KAAA,EAAM;AACb,MAAA,IAAI;AAAE,QAAA,MAAA,CAAO,GAAA,IAAM;AAAA,MAAE,CAAA,CAAA,MAAQ;AAAA,MAAqB;AAAA,IACpD;AAAA,GACF;AACF;AAiBA,eAAe,qBAAsB,CAAA,EAA8C;AACjF,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI;AAAE,IAAA,OAAA,GAAU,MAAM,OAAO,SAAS,CAAA;AAAA,EAAE,CAAA,CAAA,MAClC;AACJ,IAAA,MAAM,IAAI,MAAM,kHAA6G,CAAA;AAAA,EAC/H;AACA,EAAA,MAAM,EAAE,OAAA,EAAS,YAAA,EAAc,qBAAA,EAAuB,kBAAiB,GAAI,OAAA;AAM3E,EAAA,MAAM,OAAO,MAAY;AAAA,EAAC,CAAA;AAC1B,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,KAAA,EAAO,QAAA;AAAA,IACP,KAAA,GAAO;AAAE,MAAA,OAAO,MAAA;AAAA,IAAO,CAAA;AAAA,IACvB,KAAA,EAAO,IAAA;AAAA,IAAM,KAAA,EAAO,IAAA;AAAA,IAAM,IAAA,EAAM,IAAA;AAAA,IAAM,IAAA,EAAM,IAAA;AAAA,IAAM,KAAA,EAAO,IAAA;AAAA,IAAM,KAAA,EAAO;AAAA,GACxE;AAGA,EAAA,MAAM,cAAyB,EAAC;AAChC,EAAA,IAAI,KAAA,GAAgD,IAAA;AAEpD,EAAA,MAAM,MAAA,GAAuB;AAAA,IAC3B,IAAI,IAAA,GAAM;AAAE,MAAA,OAAO,KAAA,EAAO,IAAA,GAAO,EAAE,EAAA,EAAI,KAAA,CAAM,IAAA,CAAK,EAAA,EAAI,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,IAAA,IAAQ,MAAA,EAAU,GAAI,IAAA;AAAA,IAAK,CAAA;AAAA,IAClG,EAAA,EAAI,EAAE,EAAA,EAAI,CAAE,IAAI,EAAA,KAAQ;AAAE,MAAA,WAAA,CAAY,KAAM,EAAG,CAAA;AAAA,IAAE,CAAA,EAAE;AAAA,IACnD,WAAA,EAAa,CAAE,GAAA,EAAK,OAAA,KAAa;AAC/B,MAAA,IAAI,CAAC,OAAQ,OAAO,OAAA,CAAQ,OAAQ,IAAI,KAAA,CAAM,+BAA+B,CAAE,CAAA;AAC/E,MAAA,OAAO,KAAA,CAAM,WAAA,CAAa,GAAA,EAAK,OAAQ,CAAA;AAAA,IACzC,CAAA;AAAA,IACA,kBAAA,EAAoB,CAAEA,MAAAA,EAAO,GAAA,KAAS,KAAA,EAAO,mBAAoBA,MAAAA,EAAO,GAAI,CAAA,IAAK,OAAA,CAAQ,OAAA,EAAQ;AAAA,IACjG,GAAA,EAAK,MAAM,KAAA,EAAO,GAAA,CAAK,MAAU;AAAA,GACnC;AAEA,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,KAAc,MAAM,qBAAA,CAAuB,EAAE,QAAS,CAAA;AAErE,EAAA,MAAM,IAAI,OAAA,CAAe,CAAE,WAAA,EAAa,UAAA,KAAgB;AACtD,IAAA,IAAI,MAAA,GAAS,KAAA;AAEb,IAAA,SAAS,OAAA,GAAgB;AACvB,MAAA,MAAM,OAAO,YAAA,CAAc,EAAE,IAAA,EAAM,KAAA,EAAO,QAA0B,CAAA;AACpE,MAAA,KAAA,GAAQ,IAAA;AAER,MAAA,IAAA,CAAK,EAAA,CAAG,EAAA,CAAG,cAAA,EAAgB,SAAU,CAAA;AACrC,MAAA,IAAA,CAAK,EAAA,CAAG,EAAA,CAAG,iBAAA,EAAmB,CAAA,CAAA,KAAK;AAAE,QAAA,KAAA,MAAW,EAAA,IAAM,WAAA,EAAc,EAAA,CAAI,CAAW,CAAA;AAAA,MAAE,CAAE,CAAA;AAEvF,MAAA,IAAA,CAAK,EAAA,CAAG,EAAA,CAAG,mBAAA,EAAqB,CAAA,MAAA,KAAU;AACxC,QAAA,MAAM,EAAE,UAAA,EAAY,cAAA,EAAgB,EAAA,EAAG,GAAI,MAAA;AAI3C,QAAA,IAAI,EAAA,EAAK,KAAK,OAAA,CAAS,EAAA,EAAI,EAAE,GAAI,CAAA;AAEjC,QAAA,IAAI,UAAA,KAAe,MAAA,IAAU,CAAC,MAAA,EAAQ;AACpC,UAAA,MAAA,GAAS,IAAA;AACT,UAAA,WAAA,EAAY;AAAA,QACd;AACA,QAAA,IAAI,eAAe,OAAA,EAAQ;AACzB,UAAA,MAAM,IAAA,GAAS,cAAA,EAAgB,KAAA,EAA6D,MAAA,EAAQ,UAAA;AACpG,UAAA,IAAI,IAAA,KAAS,iBAAiB,SAAA,EAAW;AACvC,YAAA,MAAM,GAAA,GAAM,IAAI,KAAA,CAAM,uFAAkF,CAAA;AACxG,YAAA,CAAA,CAAE,GAAA,CAAK,IAAI,OAAQ,CAAA;AACnB,YAAA,IAAI,CAAC,MAAA,EAAS,UAAA,CAAY,GAAI,CAAA;AAC9B,YAAA;AAAA,UACF;AACA,UAAA,IAAI,CAAA,CAAE,WAAU,EAAG;AACjB,YAAA,CAAA,CAAE,GAAA,CAAI,CAAA,0BAAA,EAA8B,IAAA,IAAQ,GAAI,CAAA,2BAAA,CAAmB,CAAA;AACnE,YAAA,UAAA,CAAY,SAAS,GAAM,CAAA;AAAA,UAC7B;AAAA,QACF;AAAA,MACF,CAAE,CAAA;AAAA,IACJ;AAEA,IAAA,OAAA,EAAQ;AAAA,EACV,CAAE,CAAA;AAEF,EAAA,OAAO,MAAA;AACT;AAGA,eAAe,OAAA,CAAS,IAAY,GAAA,EAA4C;AAC9E,EAAA,GAAA,CAAI,uFAAwE,CAAA;AAC5E,EAAA,IAAI;AAEF,IAAA,MAAM,GAAA,GAAQ,MAAM,OAAO,iBAA4B,CAAA;AACtD,IAAA,CAAE,GAAA,CAAI,WAAW,GAAA,EAAM,QAAA,CAAU,IAAI,EAAE,KAAA,EAAO,MAAO,CAAA;AAAA,EACxD,CAAA,CAAA,MACM;AACJ,IAAA,GAAA,CAAI,CAAA;AAAA,EAAuD,EAAG,CAAA,CAAE,CAAA;AAAA,EAClE;AACF","file":"whatsapp.js","sourcesContent":["// ─────────────────────────────────────────────────────────────\n// src/channels/roster.ts — who the Will knows on a platform, and where\n// ─────────────────────────────────────────────────────────────\n//\n// The mind knows *entities*; a platform knows user ids and channels. The roster\n// is the durable seam between them: for each entity the Will has met on a\n// channel it records how to reach them again — so a *proactive* utterance\n// (`message.to` from the mind's own initiative) can find its person after a\n// restart, not just within one session.\n//\n// It persists as a small JSON file next to the PMA artifact. Writes are\n// throttled (the file is advisory routing state, not cognition — losing the\n// last few seconds costs a fallback delivery, never memory).\n// ─────────────────────────────────────────────────────────────\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\n\n/** How to reach one entity on the platform. */\nexport interface RosterEntry {\n /** The mind-side entity id, e.g. 'discord:80351110224678912'. */\n entityId: string\n /** The platform-side user id. */\n userId: string\n /** Last display name seen (advisory — the *learned* name lives in the mind). */\n displayName?: string\n /** DM channel id, once one is known. */\n dmChannelId?: string\n /** Last shared (guild) channel this entity spoke in. */\n lastChannelId?: string\n /** Epoch ms of the last message seen from them. */\n lastSeenAt: number\n}\n\nconst FLUSH_MS = 2_000\n\nexport class ChannelRoster {\n private entries = new Map<string, RosterEntry>()\n private dirty = false\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor( private readonly path: string ) {\n if( existsSync( path ) ){\n try {\n const raw = JSON.parse( readFileSync( path, 'utf8') ) as RosterEntry[]\n for( const e of Array.isArray( raw ) ? raw : [] ) this.entries.set( e.entityId, e )\n }\n catch { /* a corrupt roster is not worth failing a boot over — start fresh */ }\n }\n }\n\n /** Upsert what we just learned about an entity; schedules a throttled flush. */\n record( update: { entityId: string; userId: string } & Partial<Omit<RosterEntry, 'entityId' | 'userId'>> ): RosterEntry {\n const prev = this.entries.get( update.entityId )\n const next: RosterEntry = {\n lastSeenAt: Date.now(),\n ...prev,\n ...Object.fromEntries( Object.entries( update ).filter( ( [ , v ] ) => v !== undefined ) ) as typeof update,\n }\n this.entries.set( next.entityId, next )\n this.dirty = true\n if( !this.timer ){\n this.timer = setTimeout( () => { this.timer = null; this.flush() }, FLUSH_MS )\n this.timer.unref?.()\n }\n return next\n }\n\n resolve( entityId: string ): RosterEntry | undefined {\n return this.entries.get( entityId )\n }\n\n all(): RosterEntry[] {\n return [ ...this.entries.values() ]\n }\n\n /** Write to disk now (no-op when clean). Called by bridges on close. */\n flush(): void {\n if( !this.dirty ) return\n try {\n mkdirSync( dirname( this.path ), { recursive: true } )\n writeFileSync( this.path, JSON.stringify( this.all(), null, 2 ) )\n this.dirty = false\n }\n catch { /* advisory state — never take the mind down over it */ }\n }\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/types.ts — the channel-bridge contract\n// ─────────────────────────────────────────────────────────────\n//\n// A channel bridge puts a Will *in a place where people already are* (Discord,\n// Telegram, Slack, …). It is a host surface, not a cognition surface: it turns\n// platform messages into `perceive` stimuli and delivers the Will's projected\n// utterances back — nothing more. The paradigm survives the crossing:\n//\n// • every platform user is an entity the Will comes to know (`from`),\n// with a *learned* name (`speaker`) — never a placeholder;\n// • every platform channel/DM is a conversation thread (`thread`);\n// • the Will decides when to speak. Silence is a valid outcome, so a\n// bridge never fabricates a reply and never times a message out into\n// an error.\n//\n// Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):\n// they wrap the SDK facade, not the stem.\n// ─────────────────────────────────────────────────────────────\n\n/** A running connection between one Will and one platform. */\nexport interface ChannelBridge {\n /** Platform kind, e.g. 'discord'. */\n readonly kind: string\n /** Connect and start relaying. Resolves once the bridge is live. */\n start(): Promise<void>\n /** Disconnect and release resources. Idempotent. */\n close(): Promise<void>\n}\n\n// ── Attachments ──────────────────────────────────────────────────────────────\n//\n// People hand over documents as well as speech, and some platforms *manufacture*\n// them: Discord silently turns a long pasted markdown block into a `.md` upload.\n// A bridge that reads only the text body sees such a message as empty and — worse\n// — as nothing at all, so the person appears to have gone silent.\n//\n// What a bridge does with these is deliberately modest. A named-but-unread file\n// is already a percept the Will can act on (\"what's in it?\"), which is the\n// paradigm-correct outcome and strictly better than silence. Inlining text is an\n// upgrade on top, never a precondition.\n\n/** One file riding along with a platform message. */\nexport interface ChannelAttachment {\n name: string\n contentType?: string\n size?: number\n url?: string\n}\n\n/** Per-attachment inline budget. A 2 MB doc must not enter working memory whole. */\nconst INLINE_CHAR_CAP = 24_000\n/** How many text attachments to inline from one message. */\nconst INLINE_COUNT_CAP = 4\n\nconst TEXTUAL_EXT = /\\.(md|markdown|txt|text|json|jsonl|csv|tsv|ya?ml|log|ini|toml)$/i\n\n/** Is this something we can meaningfully read as text? */\nexport function isTextual( a: ChannelAttachment ): boolean {\n const ct = a.contentType?.split(';')[0]?.trim().toLowerCase() ?? ''\n if( ct.startsWith('text/') ) return true\n if( ct === 'application/json' || ct === 'application/x-yaml' ) return true\n // Discord's own markdown uploads arrive as text/plain, but trust the extension\n // too — content types from platforms are advisory at best.\n return TEXTUAL_EXT.test( a.name )\n}\n\nfunction humanSize( bytes?: number ): string {\n if( bytes == null ) return ''\n return bytes < 1024 ? `${ bytes } B`\n : bytes < 1024 * 1024 ? `${ ( bytes / 1024 ).toFixed( 1 ) } KB`\n : `${ ( bytes / 1024 / 1024 ).toFixed( 1 ) } MB`\n}\n\n/**\n * Render attachments into perceivable text.\n *\n * `fetchText` is supplied by the bridge, not by this module — the decision about\n * which hosts are safe to fetch from is platform knowledge, and a helper that\n * fetched arbitrary URLs found in inbound messages would be an open redirect\n * into the Will's perception. Omit it and attachments are named, never read.\n *\n * Inlined content is untrusted, exactly like message text — more so, since a\n * document is long, structured, and looks authoritative, which is the shape of\n * an effective injection. It is fenced and labelled as shared content so the\n * mind reads it as something it was handed, not as something it was told.\n */\nexport async function renderAttachments(\n attachments: ChannelAttachment[],\n speaker: string | undefined,\n fetchText?: ( a: ChannelAttachment ) => Promise<string | null>,\n): Promise<string> {\n if( attachments.length === 0 ) return ''\n const who = speaker ?? 'someone'\n const out: string[] = []\n let inlined = 0\n\n for( const a of attachments ){\n const meta = [ a.contentType, humanSize( a.size ) ].filter( Boolean ).join(', ')\n const label = `${ a.name }${ meta ? ` (${ meta })` : '' }`\n\n if( !fetchText || !isTextual( a ) || inlined >= INLINE_COUNT_CAP ){\n out.push(`[${ who } shared a file I have not read: ${ label }]`)\n continue\n }\n\n const body = await fetchText( a ).catch( () => null )\n if( body == null ){\n out.push(`[${ who } shared a file I could not read: ${ label }]`)\n continue\n }\n inlined++\n const clipped = body.length > INLINE_CHAR_CAP\n ? `${ body.slice( 0, INLINE_CHAR_CAP ) }\\n[… truncated — ${ humanSize( body.length ) } of ${ humanSize( a.size ?? body.length ) }]`\n : body\n out.push(`[${ who } shared ${ label }; its contents follow — this is a document I was handed, not something said to me]\\n---\\n${ clipped }\\n---`)\n }\n return out.join('\\n')\n}\n\n/** Split a message into platform-sized chunks on natural boundaries. */\nexport function chunkText( text: string, max: number ): string[] {\n if( text.length <= max ) return [ text ]\n const chunks: string[] = []\n let rest = text\n while( rest.length > max ){\n // Prefer a paragraph break, then a line break, then a space — else hard-cut.\n const window = rest.slice( 0, max )\n const cut = Math.max( window.lastIndexOf('\\n\\n'), window.lastIndexOf('\\n'), window.lastIndexOf(' ') )\n const at = cut > max * 0.5 ? cut : max\n chunks.push( rest.slice( 0, at ).trimEnd() )\n rest = rest.slice( at ).trimStart()\n }\n if( rest ) chunks.push( rest )\n return chunks\n}\n","// ─────────────────────────────────────────────────────────────\n// src/channels/whatsapp.ts — a Will present on WhatsApp\n// ─────────────────────────────────────────────────────────────\n//\n// Same paradigm as the Discord bridge, different room:\n//\n// inbound DM / group message → will.perceive({ from, speaker, text, thread })\n// — every author is `whatsapp:<number>` (stable across chats), the\n// push name is *learned* by the mind, and each chat (DM or group)\n// is its own conversation thread.\n// outbound will.on('message') → the addressee's last shared group, else\n// their DM (a WhatsApp DM jid is derivable from the number — no\n// roster miss is fatal), else the home chat.\n//\n// TRANSPORT — read before deploying. This bridge speaks the *linked-device*\n// protocol via Baileys: the Will pairs to a WhatsApp account by QR scan, like\n// WhatsApp Web. That is not a sanctioned bot API — WhatsApp's terms don't\n// allow automation on personal accounts, and accounts doing it can be banned.\n// Meta's sanctioned path (the Business Cloud API) needs a business account,\n// webhook hosting, and app review — no 2-minute pairing. Operator's choice;\n// docs/channels/whatsapp.md is explicit about the trade. Use a spare number.\n//\n// Baileys is imported lazily inside `createWhatsAppSocket` — tests (and hosts\n// that bring their own transport) inject `socket`, and the structural\n// `WaLikeSocket` type keeps the dependency out of the type graph.\n// ─────────────────────────────────────────────────────────────\n\nimport type { Will, WillMessage } from '#sdk/will'\nimport { ChannelRoster } from '#channels/roster'\nimport { chunkText, type ChannelBridge } from '#channels/types'\n\n/** WhatsApp's hard per-message cap. */\nconst WHATSAPP_MESSAGE_LIMIT = 65_536\n\n// ── The slice of a Baileys socket the bridge actually uses (structural) ──────\n\nexport interface WaLikeMessage {\n key: {\n remoteJid?: string | null\n fromMe?: boolean | null\n /** Sender jid inside a group (absent in DMs). */\n participant?: string | null\n }\n /** Sender's display (\"push\") name. */\n pushName?: string | null\n messageStubType?: number | null\n message?: {\n conversation?: string | null\n extendedTextMessage?: { text?: string | null; contextInfo?: { mentionedJid?: string[] | null } | null } | null\n imageMessage?: { caption?: string | null } | null\n videoMessage?: { caption?: string | null } | null\n } | null\n}\n\nexport interface WaLikeSocket {\n /** The paired account, once connected. id like '4915…:12@s.whatsapp.net'. */\n user?: { id: string; name?: string } | null\n ev: { on( event: 'messages.upsert', fn: ( u: { messages: WaLikeMessage[]; type?: string } ) => void ): unknown }\n sendMessage( jid: string, content: { text: string } ): Promise<unknown>\n sendPresenceUpdate?( state: 'composing' | 'paused', jid?: string ): Promise<unknown>\n /** Tear the connection down (does not unlink the device). */\n end?( err?: Error ): void\n}\n\n// ── Options ──────────────────────────────────────────────────────────────────\n\nexport interface WhatsAppBridgeOptions {\n /** Chat jids the Will inhabits (groups `…@g.us`, DMs `…@s.whatsapp.net`). Unset = every chat. */\n chats?: string[]\n /** Perceive group messages only when the Will is @mentioned (DMs always perceived). */\n mentionOnly?: boolean\n /** Fallback chat jid for utterances with no reachable addressee. */\n homeChatId?: string\n /** Linked-device credentials dir (default: ./.will/<willId>.wa-auth). QR pairs on first run. */\n authPath?: string\n /** Roster path (default: ./.will/<willId>.whatsapp.json). */\n rosterPath?: string\n /** Test / power-user seam: bring your own socket; Baileys is never imported. */\n socket?: WaLikeSocket\n log?: ( msg: string ) => void\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nconst isGroupJid = ( jid: string ): boolean => jid.endsWith('@g.us')\n\n/** '4915123:7@s.whatsapp.net' → '4915123' (device + server stripped). */\nconst bareId = ( jid: string ): string => jid.split('@')[0]!.split(':')[0]!\n\n/** A DM jid is derivable from the bare number — WhatsApp's gift to proactivity. */\nconst dmJidFor = ( userId: string ): string => `${ userId }@s.whatsapp.net`\n\nfunction textOf( m: WaLikeMessage ): string {\n const msg = m.message\n return msg?.conversation\n ?? msg?.extendedTextMessage?.text\n ?? msg?.imageMessage?.caption\n ?? msg?.videoMessage?.caption\n ?? ''\n}\n\n// ── The bridge ───────────────────────────────────────────────────────────────\n\n/**\n * Connect a Will to WhatsApp over the linked-device protocol. First run prints\n * a QR to pair (Settings → Linked devices → Link a device); credentials persist\n * in `authPath` so later runs reconnect silently. Close via the returned\n * `ChannelBridge.close()` — the Will keeps ticking; it only loses this surface.\n */\nexport async function connectWhatsApp( will: Will, opts: WhatsAppBridgeOptions = {} ): Promise<ChannelBridge> {\n const log = opts.log ?? ( ( m: string ) => console.error(`[will:whatsapp] ${ m }`) )\n const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.whatsapp.json`)\n const allowed = opts.chats?.length ? new Set( opts.chats ) : null\n\n let closed = false\n const socket = opts.socket ?? await createWhatsAppSocket( {\n authPath: opts.authPath ?? `.will/${ will.id }.wa-auth`,\n log,\n stillOpen: () => !closed,\n } )\n\n /** The most recently active allowed chat — last-resort proactive target. */\n let lastActiveChatId: string | null = opts.homeChatId ?? null\n\n // ── inbound: platform message → stimulus ──────────────────────────────────\n socket.ev.on('messages.upsert', ( { messages, type } ) => {\n if( type && type !== 'notify') return // history sync/corrections are not new percepts\n for( const m of messages ) void onMessage( m )\n } )\n\n async function onMessage( m: WaLikeMessage ): Promise<void> {\n const jid = m.key.remoteJid\n if( !jid || m.key.fromMe || !m.message || m.messageStubType ) return\n if( jid.endsWith('@broadcast') || jid.endsWith('@newsletter') ) return // stories/broadcasts aren't a room the Will is in\n if( allowed && !allowed.has( jid ) ) return\n\n const isGroup = isGroupJid( jid )\n const senderJid = isGroup ? m.key.participant : jid\n if( !senderJid ) return\n\n const selfId = socket.user ? bareId( socket.user.id ) : null\n const mentioned = m.message.extendedTextMessage?.contextInfo?.mentionedJid ?? []\n const addressed = !isGroup || ( selfId != null && mentioned.some( j => bareId( j ) === selfId ) )\n if( opts.mentionOnly && !addressed ) return\n\n const userId = bareId( senderJid )\n const entityId = `whatsapp:${ userId }`\n const speaker = m.pushName ?? undefined\n\n roster.record( {\n entityId,\n userId,\n ...( speaker ? { displayName: speaker } : {} ),\n ...( isGroup ? { lastChannelId: jid } : { dmChannelId: jid } ),\n } )\n if( isGroup ) lastActiveChatId = jid\n\n // Same rule as Discord: a presence cue only when addressed — the mind may\n // still choose silence, and 'composing' expires on its own.\n if( addressed ) await socket.sendPresenceUpdate?.('composing', jid ).catch( () => {} )\n\n const text = textOf( m )\n if( !text.trim() ) return\n\n await will.perceive( {\n text,\n from: entityId,\n thread: `whatsapp:${ jid }`,\n // A WhatsApp group jid ends in `@g.us`; anything else is a one-to-one chat.\n direct: !jid.endsWith('@g.us'),\n ...( speaker ? { speaker } : {} ),\n } )\n }\n\n // ── outbound: projected utterance → the addressee ─────────────────────────\n will.on('message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )\n\n async function deliver( m: WillMessage ): Promise<void> {\n const peer = m.to ? roster.resolve( m.to ) : undefined\n const chunks = chunkText( m.content, WHATSAPP_MESSAGE_LIMIT )\n\n // The chat they actually spoke in → last shared group → known DM → DM derived\n // from the entity id itself → home chat → last active chat. Unlike Discord, an\n // unmet-but-addressed entity is still reachable: `whatsapp:<number>` implies\n // its DM jid.\n //\n // `m.thread` leads because everything after it is a guess about where this\n // person usually is, and a reply belongs in the room the question was asked\n // in. The Discord bridge had the identical ordering and answered a DM in a\n // shared server channel — from the operator's side, silence.\n const replyTo = m.thread?.startsWith('whatsapp:') ? m.thread.slice('whatsapp:'.length ) : undefined\n const derivedDm = m.to?.startsWith('whatsapp:') ? dmJidFor( m.to.slice('whatsapp:'.length ) ) : undefined\n const targets = [ replyTo, peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? undefined, lastActiveChatId ?? undefined ]\n for( const jid of targets ){\n if( !jid ) continue\n try {\n for( const chunk of chunks ) await socket.sendMessage( jid, { text: chunk } )\n return\n }\n catch { /* try the next route */ }\n }\n log(`no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)`)\n }\n\n // ── lifecycle ──────────────────────────────────────────────────────────────\n return {\n kind: 'whatsapp',\n async start(): Promise<void> {\n // createWhatsAppSocket resolves already-open; an injected socket is the\n // caller's to have readied.\n log(`${ will.name } is present on WhatsApp${ socket.user ? ` as ${ bareId( socket.user.id ) }` : '' }`)\n },\n async close(): Promise<void> {\n if( closed ) return\n closed = true\n roster.flush()\n try { socket.end?.() } catch { /* already down */ }\n },\n }\n}\n\n// ── The real transport (Baileys, lazily imported) ────────────────────────────\n\ninterface SocketFactoryOpts {\n authPath: string\n log: ( msg: string ) => void\n /** Reconnect only while the bridge is open. */\n stillOpen: () => boolean\n}\n\n/**\n * A reconnect-stable facade over Baileys. Baileys' own pattern replaces the\n * socket object on every reconnect; the facade keeps one stable identity the\n * bridge can hold, re-attaching its subscribers to each inner incarnation.\n * Resolves once the first connection is open (after QR pairing on first run).\n */\nasync function createWhatsAppSocket( o: SocketFactoryOpts ): Promise<WaLikeSocket> {\n let baileys: typeof import('baileys')\n try { baileys = await import('baileys') }\n catch {\n throw new Error('baileys is not installed (it is an optionalDependency) — run `bun add baileys` / `npm i baileys` and retry.')\n }\n const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = baileys\n\n // Baileys logs through pino at info by default — JSON noise all over the\n // host's stderr. A minimal pino-shaped silent logger (child() returns\n // itself, every level a no-op) is what it actually needs; hand-rolling it\n // beats importing a transitive dep that may not resolve from dist/.\n const noop = (): void => {}\n const logger = {\n level: 'silent',\n child(){ return logger },\n trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop,\n }\n\n type Handler = ( u: { messages: WaLikeMessage[]; type?: string } ) => void\n const subscribers: Handler[] = []\n let inner: ReturnType<typeof makeWASocket> | null = null\n\n const facade: WaLikeSocket = {\n get user(){ return inner?.user ? { id: inner.user.id, name: inner.user.name ?? undefined } : null },\n ev: { on: ( _e, fn ) => { subscribers.push( fn ) } },\n sendMessage: ( jid, content ) => {\n if( !inner ) return Promise.reject( new Error('whatsapp socket not connected') )\n return inner.sendMessage( jid, content )\n },\n sendPresenceUpdate: ( state, jid ) => inner?.sendPresenceUpdate( state, jid ) ?? Promise.resolve(),\n end: () => inner?.end( undefined ),\n }\n\n const { state, saveCreds } = await useMultiFileAuthState( o.authPath )\n\n await new Promise<void>( ( resolveOpen, rejectOpen ) => {\n let opened = false\n\n function connect(): void {\n const sock = makeWASocket( { auth: state, logger: logger as never } )\n inner = sock\n\n sock.ev.on('creds.update', saveCreds )\n sock.ev.on('messages.upsert', u => { for( const fn of subscribers ) fn( u as never ) } )\n\n sock.ev.on('connection.update', update => {\n const { connection, lastDisconnect, qr } = update as {\n connection?: string; lastDisconnect?: { error?: unknown }; qr?: string\n }\n\n if( qr ) void printQr( qr, o.log )\n\n if( connection === 'open' && !opened ){\n opened = true\n resolveOpen()\n }\n if( connection === 'close'){\n const code = ( lastDisconnect?.error as { output?: { statusCode?: number } } | undefined )?.output?.statusCode\n if( code === DisconnectReason.loggedOut ){\n const err = new Error('WhatsApp unlinked this device (logged out) — delete the auth dir and pair again.')\n o.log( err.message )\n if( !opened ) rejectOpen( err )\n return\n }\n if( o.stillOpen() ){\n o.log(`connection closed (status ${ code ?? '?' }) — reconnecting…`)\n setTimeout( connect, 3_000 )\n }\n }\n } )\n }\n\n connect()\n } )\n\n return facade\n}\n\n/** QR to the terminal; falls back to the raw pairing string if the tiny renderer is absent. */\nasync function printQr( qr: string, log: ( m: string ) => void ): Promise<void> {\n log('pair this device: WhatsApp → Settings → Linked devices → Link a device')\n try {\n type QrModule = { generate( s: string, o: { small: boolean } ): void }\n const qrt = ( await import('qrcode-terminal' as string ) ) as QrModule & { default?: QrModule }\n ;( qrt.default ?? qrt ).generate( qr, { small: true } )\n }\n catch {\n log(`qrcode-terminal not installed — raw pairing code:\\n${ qr }`)\n }\n}\n"]}
|