@mindot/will 0.8.0 → 0.10.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.
Files changed (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -0,0 +1,177 @@
1
+ import { W as Will } from '../../will-evj9_vrd.js';
2
+ import { C as ChannelBridge } from '../../types-E9-HV-SW.js';
3
+
4
+ interface DiscordLikeChannel {
5
+ send(content: string): Promise<unknown>;
6
+ sendTyping?(): Promise<unknown>;
7
+ /** `general` for a text channel; absent on a DM, which has no name and is a person. */
8
+ name?: string | null;
9
+ /** The thread's parent channel, so a thread reads as "#general › release-cut". */
10
+ parent?: {
11
+ name?: string | null;
12
+ } | null;
13
+ /** What the room is FOR, in the server's own words. Only read when asked. */
14
+ topic?: string | null;
15
+ /** How many are in it — a count, never a roster. */
16
+ memberCount?: number;
17
+ }
18
+ interface DiscordLikeAttachment {
19
+ name?: string | null;
20
+ contentType?: string | null;
21
+ size?: number;
22
+ url?: string;
23
+ }
24
+ interface DiscordLikeMessage {
25
+ content: string;
26
+ cleanContent?: string;
27
+ channelId: string;
28
+ guildId?: string | null;
29
+ /** The server this was said in. Its NAME is what a person calls the place. */
30
+ guild?: {
31
+ name?: string | null;
32
+ } | null;
33
+ author: {
34
+ id: string;
35
+ bot?: boolean;
36
+ username?: string;
37
+ displayName?: string;
38
+ };
39
+ member?: {
40
+ displayName?: string;
41
+ } | null;
42
+ mentions?: {
43
+ has(userId: string): boolean;
44
+ };
45
+ channel: DiscordLikeChannel;
46
+ /**
47
+ * Files riding with the message.
48
+ *
49
+ * discord.js hands us a `Collection`, which extends `Map` — so iterating it
50
+ * directly yields `[id, attachment]` PAIRS, not attachments. Typing this as a
51
+ * bare `Iterable` was wrong and silently produced `name: undefined` against
52
+ * the real client while passing every test, because the test fake injects an
53
+ * array. Both shapes are accepted now and normalised in `collectAttachments`.
54
+ */
55
+ attachments?: ReadonlyMap<string, DiscordLikeAttachment> | Iterable<DiscordLikeAttachment>;
56
+ }
57
+ /**
58
+ * A reaction, and the message it lands on.
59
+ *
60
+ * Both halves may be PARTIAL: Discord delivers a reaction on an uncached message
61
+ * with almost every field empty, which is the normal case for a message the bot
62
+ * sent before its current process started. `fetch()` fills it in, and the handler
63
+ * must call it rather than reading through the hole.
64
+ */
65
+ interface DiscordLikeReaction {
66
+ emoji: {
67
+ name?: string | null;
68
+ id?: string | null;
69
+ };
70
+ message: DiscordLikeMessage & {
71
+ partial?: boolean;
72
+ author?: {
73
+ id: string;
74
+ bot?: boolean;
75
+ username?: string;
76
+ displayName?: string;
77
+ };
78
+ fetch?(): Promise<DiscordLikeReaction['message']>;
79
+ };
80
+ partial?: boolean;
81
+ fetch?(): Promise<DiscordLikeReaction>;
82
+ }
83
+ interface DiscordLikeReactor {
84
+ id: string;
85
+ bot?: boolean;
86
+ username?: string;
87
+ displayName?: string;
88
+ }
89
+ interface DiscordLikeClient {
90
+ user: {
91
+ id: string;
92
+ setPresence?(p: unknown): void;
93
+ } | null;
94
+ /** discord.js ≥14.22; polled so we needn't subscribe to the deprecated `ready`. */
95
+ isReady?(): boolean;
96
+ on(event: 'messageCreate', fn: (m: DiscordLikeMessage) => void): unknown;
97
+ on(event: 'messageReactionAdd', fn: (r: DiscordLikeReaction, u: DiscordLikeReactor) => void): unknown;
98
+ once(event: string, fn: () => void): unknown;
99
+ login(token: string): Promise<unknown>;
100
+ destroy(): Promise<unknown> | void;
101
+ channels: {
102
+ fetch(id: string): Promise<unknown>;
103
+ };
104
+ users: {
105
+ fetch(id: string): Promise<{
106
+ send(content: string): Promise<unknown>;
107
+ }>;
108
+ };
109
+ }
110
+ interface DiscordBridgeOptions {
111
+ /** Bot token (Discord developer portal). Unused when `client` is injected pre-logged-in. */
112
+ token?: string;
113
+ /**
114
+ * Channel ids the Will inhabits. Unset — or the single wildcard `'*'` — means
115
+ * every channel it can see, so adding it to a new channel in Discord is enough.
116
+ * A list restrains it to exactly those, and a message anywhere else is dropped
117
+ * at the bridge: the Will never perceives it and its silence there is not a choice.
118
+ */
119
+ channels?: readonly string[];
120
+ /**
121
+ * Where the Will only perceives guild messages that @mention it. DMs are always
122
+ * perceived either way.
123
+ *
124
+ * `true` — everywhere
125
+ * `[ 'id', … ]` — only in those channels; it listens openly elsewhere
126
+ * omitted / `false` — nowhere
127
+ *
128
+ * The list form is what makes a wide-open roster usable: present in every channel,
129
+ * but a quiet participant in the busy ones.
130
+ */
131
+ mentionOnly?: boolean | readonly string[];
132
+ /** Fallback channel for utterances with no reachable addressee. */
133
+ homeChannelId?: string;
134
+ /** Roster path (default: ./.will/<willId>.discord.json). */
135
+ rosterPath?: string;
136
+ /**
137
+ * Read the contents of text-like attachments (.md, .txt, .json, …) into the
138
+ * percept, rather than only naming them. Default true.
139
+ *
140
+ * Only Discord's own CDN is ever fetched, and only up to a size cap. Set false
141
+ * for a bridge that should never pull remote bytes — the Will still perceives
142
+ * that a file arrived and can ask about it.
143
+ */
144
+ readAttachments?: boolean;
145
+ /** Test / power-user seam: bring your own client; discord.js is never imported. */
146
+ client?: DiscordLikeClient;
147
+ log?: (msg: string) => void;
148
+ }
149
+ /**
150
+ * Connect a Will to Discord. Resolves once the bridge is live (logged in and
151
+ * relaying). Close it via the returned `ChannelBridge.close()` — the Will
152
+ * itself is not stopped; it simply loses this surface.
153
+ */
154
+ declare function connectDiscord(will: Will, opts: DiscordBridgeOptions): Promise<ChannelBridge>;
155
+ /**
156
+ * Parse `WILL_DISCORD_MENTION_ONLY` into the `mentionOnly` option.
157
+ *
158
+ * Accepts a boolean OR a channel list, because "only speak when spoken to" is
159
+ * rarely a whole-server property — it is how you stay present in a busy channel
160
+ * without narrating in it.
161
+ *
162
+ * `1` / `true` / `yes` → true (everywhere)
163
+ * `0` / `false` / unset → false (nowhere)
164
+ * `123,456` → only those channels
165
+ *
166
+ * Exported so every host parses it identically; the CLI and any SDK host share
167
+ * this rather than each re-deriving the syntax.
168
+ */
169
+ declare function parseMentionOnly(raw?: string): boolean | string[];
170
+ /**
171
+ * Parse `WILL_DISCORD_CHANNELS`. `*` (or unset/empty) means every channel the Will
172
+ * can see — being added to a channel in Discord is then all it takes. Anything else
173
+ * restrains it to exactly the ids listed.
174
+ */
175
+ declare function parseChannels(raw?: string): string[] | undefined;
176
+
177
+ export { type DiscordBridgeOptions, type DiscordLikeAttachment, type DiscordLikeChannel, type DiscordLikeClient, type DiscordLikeMessage, type DiscordLikeReaction, type DiscordLikeReactor, connectDiscord, parseChannels, parseMentionOnly };
@@ -0,0 +1,394 @@
1
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ // src/surface/channels/roster.ts
5
+ var FLUSH_MS = 2e3;
6
+ var ChannelRoster = class {
7
+ constructor(path) {
8
+ this.path = path;
9
+ if (existsSync(path)) {
10
+ try {
11
+ const raw = JSON.parse(readFileSync(path, "utf8"));
12
+ for (const e of Array.isArray(raw) ? raw : []) this.entries.set(e.entityId, e);
13
+ } catch {
14
+ }
15
+ }
16
+ }
17
+ path;
18
+ entries = /* @__PURE__ */ new Map();
19
+ dirty = false;
20
+ timer = null;
21
+ /** Upsert what we just learned about an entity; schedules a throttled flush. */
22
+ record(update) {
23
+ const prev = this.entries.get(update.entityId);
24
+ const next = {
25
+ lastSeenAt: Date.now(),
26
+ ...prev,
27
+ ...Object.fromEntries(Object.entries(update).filter(([, v]) => v !== void 0))
28
+ };
29
+ this.entries.set(next.entityId, next);
30
+ this.dirty = true;
31
+ if (!this.timer) {
32
+ this.timer = setTimeout(() => {
33
+ this.timer = null;
34
+ this.flush();
35
+ }, FLUSH_MS);
36
+ this.timer.unref?.();
37
+ }
38
+ return next;
39
+ }
40
+ resolve(entityId) {
41
+ return this.entries.get(entityId);
42
+ }
43
+ all() {
44
+ return [...this.entries.values()];
45
+ }
46
+ /** Write to disk now (no-op when clean). Called by bridges on close. */
47
+ flush() {
48
+ if (!this.dirty) return;
49
+ try {
50
+ mkdirSync(dirname(this.path), { recursive: true });
51
+ writeFileSync(this.path, JSON.stringify(this.all(), null, 2));
52
+ this.dirty = false;
53
+ } catch {
54
+ }
55
+ }
56
+ };
57
+
58
+ // src/surface/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
+ }
99
+ function chunkText(text, max) {
100
+ if (text.length <= max) return [text];
101
+ const chunks = [];
102
+ let rest = text;
103
+ while (rest.length > max) {
104
+ const window = rest.slice(0, max);
105
+ const cut = Math.max(window.lastIndexOf("\n\n"), window.lastIndexOf("\n"), window.lastIndexOf(" "));
106
+ const at = cut > max * 0.5 ? cut : max;
107
+ chunks.push(rest.slice(0, at).trimEnd());
108
+ rest = rest.slice(at).trimStart();
109
+ }
110
+ if (rest) chunks.push(rest);
111
+ return chunks;
112
+ }
113
+
114
+ // src/surface/channels/discord.ts
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;
118
+ var REACTION_QUOTE_CHARS = 140;
119
+ function roomLabel(message) {
120
+ if (!message.guildId) return void 0;
121
+ const own = message.channel?.name;
122
+ if (!own) return void 0;
123
+ const parent = message.channel?.parent?.name;
124
+ const room = parent ? `#${parent} \u203A ${own}` : `#${own}`;
125
+ const guild = message.guild?.name;
126
+ return guild ? `${room} in ${guild}` : room;
127
+ }
128
+ async function connectDiscord(will, opts) {
129
+ const log = opts.log ?? ((m) => console.error(`[will:discord] ${m}`));
130
+ const roster = new ChannelRoster(opts.rosterPath ?? `.will/${will.id}.discord.json`);
131
+ const allowed = opts.channels?.length && !opts.channels.includes("*") ? new Set(opts.channels) : null;
132
+ const mentionEverywhere = opts.mentionOnly === true;
133
+ const mentionIn = Array.isArray(opts.mentionOnly) && opts.mentionOnly.length ? new Set(opts.mentionOnly) : null;
134
+ const client = opts.client ?? await createDiscordClient();
135
+ let lastActiveChannelId = opts.homeChannelId ?? null;
136
+ client.on("messageCreate", (message) => {
137
+ void onMessage(message);
138
+ });
139
+ client.on("messageReactionAdd", (reaction, user) => {
140
+ void onReaction(reaction, user);
141
+ });
142
+ async function onReaction(reaction, user) {
143
+ const self = client.user;
144
+ if (!self || user.id === self.id || user.bot) return;
145
+ const full = reaction.partial && reaction.fetch ? await reaction.fetch().catch(() => null) : reaction;
146
+ if (!full) return;
147
+ const msg = full.message.partial && full.message.fetch ? await full.message.fetch().catch(() => null) : full.message;
148
+ if (!msg?.author) return;
149
+ if (msg.author.id !== self.id) return;
150
+ const isDM = !msg.guildId;
151
+ if (!isDM && allowed && !allowed.has(msg.channelId)) return;
152
+ const emoji = full.emoji?.name ?? (full.emoji?.id ? ":custom:" : "");
153
+ if (!emoji) return;
154
+ const who = user.displayName ?? user.username;
155
+ const said = (msg.cleanContent || msg.content || "").trim().slice(0, REACTION_QUOTE_CHARS);
156
+ const text = said ? `[${who ?? "someone"} reacted ${emoji} to what I said: "${said}"]` : `[${who ?? "someone"} reacted ${emoji} to something I said]`;
157
+ await will.sense({
158
+ text,
159
+ from: `discord:${user.id}`,
160
+ thread: `discord:${msg.channelId}`,
161
+ direct: isDM,
162
+ // Exafferent, and the near-miss is worth naming: this is ABOUT something
163
+ // she did, but it is not her doing it. Somebody else reacted. Reafference
164
+ // is the mind sensing its OWN act's consequence, not the world's response
165
+ // to that act — a reply would fail the same test for the same reason.
166
+ provenance: "exafferent",
167
+ ...roomLabel(msg) ? { threadName: roomLabel(msg) } : {},
168
+ ...who ? { speaker: who } : {}
169
+ });
170
+ }
171
+ async function onMessage(message) {
172
+ const self = client.user;
173
+ if (!self || message.author.id === self.id || message.author.bot) return;
174
+ const isDM = !message.guildId;
175
+ if (!isDM && allowed && !allowed.has(message.channelId)) return;
176
+ const addressed = isDM || (message.mentions?.has(self.id) ?? false);
177
+ if (!addressed && (mentionEverywhere || mentionIn?.has(message.channelId))) return;
178
+ const entityId = `discord:${message.author.id}`;
179
+ const speaker = message.member?.displayName ?? message.author.displayName ?? message.author.username;
180
+ roster.record({
181
+ entityId,
182
+ userId: message.author.id,
183
+ ...speaker ? { displayName: speaker } : {},
184
+ ...isDM ? { dmChannelId: message.channelId } : { lastChannelId: message.channelId }
185
+ });
186
+ if (!isDM) lastActiveChannelId = message.channelId;
187
+ if (addressed) await message.channel.sendTyping?.().catch(() => {
188
+ });
189
+ const said = (message.cleanContent || message.content).trim();
190
+ const files = collectAttachments(message);
191
+ if (!said && files.length === 0) return;
192
+ const shared = await renderAttachments(
193
+ files,
194
+ speaker,
195
+ opts.readAttachments === false ? void 0 : fetchAttachmentText
196
+ );
197
+ const text = [said, shared].filter(Boolean).join("\n");
198
+ await will.sense({
199
+ text,
200
+ from: entityId,
201
+ thread: `discord:${message.channelId}`,
202
+ // `isDM` has been computed on every inbound since this bridge shipped and
203
+ // used only to pick a roster field. It is the one fact that makes a room
204
+ // the right or wrong place to say something, and the mind never saw it —
205
+ // which is how a follow-up promised in a DM went out to #general.
206
+ direct: isDM,
207
+ // Somebody spoke. The bridge already drops her own messages (`onMessage`
208
+ // returns early on `author.id === self.id`), so nothing reafferent can
209
+ // reach this line today — but that filter is a bridge-level deletion of
210
+ // a signal she is entitled to sense, not a reason for the field to lie.
211
+ provenance: "exafferent",
212
+ ...roomLabel(message) ? { threadName: roomLabel(message) } : {},
213
+ ...speaker ? { speaker } : {}
214
+ });
215
+ }
216
+ function collectAttachments(message) {
217
+ if (!message.attachments) return [];
218
+ const source = message.attachments;
219
+ const items = typeof source.values === "function" ? source.values() : message.attachments;
220
+ const out = [];
221
+ for (const a of items)
222
+ out.push({
223
+ name: a.name ?? "unnamed",
224
+ ...a.contentType ? { contentType: a.contentType } : {},
225
+ ...a.size != null ? { size: a.size } : {},
226
+ ...a.url ? { url: a.url } : {}
227
+ });
228
+ return out;
229
+ }
230
+ async function fetchAttachmentText(a) {
231
+ if (!a.url || !isTextual(a)) return null;
232
+ let host;
233
+ try {
234
+ host = new URL(a.url).hostname;
235
+ } catch {
236
+ return null;
237
+ }
238
+ if (!DISCORD_CDN_HOSTS.has(host)) {
239
+ log(`refusing to fetch attachment '${a.name}' from non-CDN host ${host}`);
240
+ return null;
241
+ }
242
+ if (a.size != null && a.size > MAX_FETCH_BYTES) {
243
+ log(`attachment '${a.name}' is ${a.size} bytes \u2014 naming it without reading`);
244
+ return null;
245
+ }
246
+ const res = await fetch(a.url, { signal: AbortSignal.timeout(1e4) });
247
+ if (!res.ok) {
248
+ log(`attachment '${a.name}' fetch failed: ${res.status}`);
249
+ return null;
250
+ }
251
+ return (await res.text()).slice(0, MAX_FETCH_BYTES);
252
+ }
253
+ will.effector("inspect", async (_args, ctx) => {
254
+ const address = (ctx.targetAddresses ?? []).find((a) => a.startsWith("discord:"));
255
+ if (!address) return { success: false, description: "Not something I can see on Discord." };
256
+ const id = address.slice("discord:".length);
257
+ const channel = await client.channels.fetch(id).catch(() => null);
258
+ if (!channel?.name) return { success: false, description: "There is nothing here I can look up." };
259
+ const facts = [];
260
+ if (channel.topic) facts.push(`it is for: ${channel.topic}`);
261
+ if (channel.parent?.name) facts.push(`it sits under #${channel.parent.name}`);
262
+ if (typeof channel.memberCount === "number")
263
+ facts.push(`${channel.memberCount} people are in it`);
264
+ if (facts.length === 0)
265
+ return { success: false, description: `#${channel.name} has nothing recorded about it.` };
266
+ const label = roomLabel({ guildId: "g", channel }) ?? `#${channel.name}`;
267
+ return {
268
+ success: true,
269
+ description: `Looked into ${label}.`,
270
+ // The room as Discord has it, in the shape Discord has it. Not flattened
271
+ // into a sentence for the mind's benefit — a host that reshapes its own
272
+ // data is deciding what the mind may notice about it, and `observation`
273
+ // takes whatever shape the answer already had.
274
+ //
275
+ // `summary` is the one concession: it is what the executive prompt renders,
276
+ // so the host says it in words rather than leaving the mind to read JSON.
277
+ // Everything beside it stays available.
278
+ observation: {
279
+ summary: `I looked into ${label}: ${facts.join("; ")}.`,
280
+ room: label,
281
+ address,
282
+ ...channel.topic ? { topic: channel.topic } : {},
283
+ ...channel.parent?.name ? { parent: `#${channel.parent.name}` } : {},
284
+ ...typeof channel.memberCount === "number" ? { memberCount: channel.memberCount } : {}
285
+ }
286
+ };
287
+ });
288
+ let closed = false;
289
+ will.on("message", (m) => {
290
+ if (!closed) void deliver(m);
291
+ });
292
+ async function deliver(m) {
293
+ const peer = m.to ? roster.resolve(m.to) : void 0;
294
+ const chunks = chunkText(m.content, DISCORD_MESSAGE_LIMIT);
295
+ const replyTo = m.thread?.startsWith("discord:") ? m.thread.slice("discord:".length) : void 0;
296
+ const channelIds = [replyTo, peer?.lastChannelId, peer?.dmChannelId, opts.homeChannelId ?? void 0, lastActiveChannelId ?? void 0];
297
+ for (const id of channelIds) {
298
+ if (!id) continue;
299
+ try {
300
+ const channel = await client.channels.fetch(id);
301
+ if (!channel?.send) continue;
302
+ for (const chunk of chunks) await channel.send(chunk);
303
+ return;
304
+ } catch {
305
+ }
306
+ }
307
+ if (peer) {
308
+ try {
309
+ const user = await client.users.fetch(peer.userId);
310
+ for (const chunk of chunks) await user.send(chunk);
311
+ return;
312
+ } catch {
313
+ }
314
+ }
315
+ log(`no route for utterance to '${m.to}' \u2014 dropped (${m.content.length} chars)`);
316
+ }
317
+ const bridge = {
318
+ kind: "discord",
319
+ async start() {
320
+ if (!client.user) {
321
+ const ready = new Promise((resolve) => {
322
+ let poll = null;
323
+ const done = () => {
324
+ if (poll) clearInterval(poll);
325
+ resolve();
326
+ };
327
+ client.once("clientReady", done);
328
+ poll = setInterval(() => {
329
+ if (client.isReady?.()) done();
330
+ }, 100);
331
+ poll.unref?.();
332
+ });
333
+ await client.login(opts.token ?? "");
334
+ await ready;
335
+ }
336
+ log(`${will.name} is present on Discord as user ${client.user?.id}`);
337
+ },
338
+ async close() {
339
+ if (closed) return;
340
+ closed = true;
341
+ roster.flush();
342
+ await Promise.resolve(client.destroy()).catch(() => {
343
+ });
344
+ }
345
+ };
346
+ return bridge;
347
+ }
348
+ async function createDiscordClient() {
349
+ let mod;
350
+ try {
351
+ mod = await import('discord.js');
352
+ } catch {
353
+ throw new Error("discord.js is not installed (it is an optionalDependency) \u2014 run `bun add discord.js` / `npm i discord.js` and retry.");
354
+ }
355
+ const { Client, GatewayIntentBits, Partials } = mod;
356
+ return new Client({
357
+ intents: [
358
+ GatewayIntentBits.Guilds,
359
+ GatewayIntentBits.GuildMessages,
360
+ GatewayIntentBits.MessageContent,
361
+ GatewayIntentBits.DirectMessages,
362
+ // Neither is privileged, so this costs nothing to ask for — and without
363
+ // them an emoji answer never arrives and reads as being ignored.
364
+ GatewayIntentBits.GuildMessageReactions,
365
+ GatewayIntentBits.DirectMessageReactions
366
+ ],
367
+ partials: [
368
+ Partials.Channel,
369
+ // DMs arrive on uncached channels
370
+ // A reaction on a message this process did not cache — i.e. anything said
371
+ // before the last restart — is delivered partial, and without these the
372
+ // event is dropped before the handler sees it. That is precisely the
373
+ // long-running conversation the answered loop exists for.
374
+ Partials.Message,
375
+ Partials.Reaction
376
+ ]
377
+ });
378
+ }
379
+ function parseMentionOnly(raw) {
380
+ const v = raw?.trim();
381
+ if (!v) return false;
382
+ if (/^(1|true|yes)$/i.test(v)) return true;
383
+ if (/^(0|false|no)$/i.test(v)) return false;
384
+ const ids = v.split(",").map((s) => s.trim()).filter(Boolean);
385
+ return ids.length ? ids : false;
386
+ }
387
+ function parseChannels(raw) {
388
+ const ids = raw?.split(",").map((s) => s.trim()).filter(Boolean);
389
+ return ids?.length ? ids : void 0;
390
+ }
391
+
392
+ export { connectDiscord, parseChannels, parseMentionOnly };
393
+ //# sourceMappingURL=discord.js.map
394
+ //# sourceMappingURL=discord.js.map