@mindot/will 0.4.0 → 0.6.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 (135) hide show
  1. package/README.md +32 -12
  2. package/dist/channels/discord.d.ts +2 -11
  3. package/dist/channels/discord.js.map +1 -1
  4. package/dist/channels/whatsapp.d.ts +72 -0
  5. package/dist/channels/whatsapp.js +252 -0
  6. package/dist/channels/whatsapp.js.map +1 -0
  7. package/dist/cli.js +837 -91
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +620 -79
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/effectors.d.ts +1 -1
  13. package/dist/mcp/effectors.js.map +1 -1
  14. package/dist/types-E9-HV-SW.d.ts +11 -0
  15. package/dist/{will-D-slky1N.d.ts → will-Bikuk4s2.d.ts} +56 -12
  16. package/package.json +12 -3
  17. package/src/channels/discord.ts +11 -11
  18. package/src/channels/roster.ts +1 -1
  19. package/src/channels/types.ts +1 -1
  20. package/src/channels/whatsapp.ts +318 -0
  21. package/src/cli.ts +46 -23
  22. package/src/cognition/acp.ts +28 -0
  23. package/src/cognition/agency/consequence.ts +174 -0
  24. package/src/cognition/agency/engines/action.selector.ts +301 -48
  25. package/src/cognition/agency/engines/affordance.synthesizer.ts +26 -16
  26. package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
  27. package/src/cognition/agency/engines/motor.schema.executor.ts +86 -14
  28. package/src/cognition/agency/engines/reafference.engine.ts +122 -18
  29. package/src/cognition/agency/execution.primitives.ts +13 -13
  30. package/src/cognition/agency/proactive.communicator.ts +1 -1
  31. package/src/cognition/agency/reconcile.learning.ts +1 -1
  32. package/src/cognition/agency/revocation.ts +79 -0
  33. package/src/cognition/agency/schemas/repertoire.ts +2 -2
  34. package/src/cognition/agency/selection.scoring.ts +25 -9
  35. package/src/cognition/bus.ts +2 -2
  36. package/src/cognition/completion.inbox.ts +1 -1
  37. package/src/cognition/config.mirror.entities.ts +5 -5
  38. package/src/cognition/event.log.ts +2 -2
  39. package/src/cognition/event.schemas.ts +67 -67
  40. package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
  41. package/src/cognition/faculties/affective.blender.ts +36 -4
  42. package/src/cognition/faculties/attachment.evaluator.ts +6 -6
  43. package/src/cognition/faculties/attention.allocator.ts +63 -8
  44. package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
  45. package/src/cognition/faculties/bias.detector.ts +7 -7
  46. package/src/cognition/faculties/circadian.oscillator.ts +3 -3
  47. package/src/cognition/faculties/confidence.calibrator.ts +7 -7
  48. package/src/cognition/faculties/dream.simulator.ts +5 -5
  49. package/src/cognition/faculties/empathy.simulator.ts +7 -7
  50. package/src/cognition/faculties/energy.regulator.ts +4 -4
  51. package/src/cognition/faculties/episodic.consolidator.ts +6 -6
  52. package/src/cognition/faculties/executive.engine/commands.ts +6 -6
  53. package/src/cognition/faculties/executive.engine/context.ts +42 -42
  54. package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
  55. package/src/cognition/faculties/executive.engine/engine.ts +27 -26
  56. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
  57. package/src/cognition/faculties/executive.engine/facet.ts +18 -18
  58. package/src/cognition/faculties/executive.engine/gating.ts +2 -2
  59. package/src/cognition/faculties/executive.engine/messages.ts +3 -3
  60. package/src/cognition/faculties/executive.engine/parser.ts +11 -11
  61. package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
  62. package/src/cognition/faculties/exteroception.ts +99 -5
  63. package/src/cognition/faculties/forgetting.curve.ts +3 -3
  64. package/src/cognition/faculties/frustration.evaluator.ts +6 -6
  65. package/src/cognition/faculties/goal.manager.ts +15 -15
  66. package/src/cognition/faculties/inhibition.controller.ts +3 -3
  67. package/src/cognition/faculties/interoception.ts +3 -3
  68. package/src/cognition/faculties/introspection.engine.ts +3 -3
  69. package/src/cognition/faculties/known.entity.tracker.ts +15 -15
  70. package/src/cognition/faculties/loss.evaluator.ts +2 -2
  71. package/src/cognition/faculties/moral.evaluator.ts +4 -4
  72. package/src/cognition/faculties/novelty.detector.ts +4 -4
  73. package/src/cognition/faculties/persona.consolidator.ts +12 -12
  74. package/src/cognition/faculties/planning.engine/engine.ts +47 -47
  75. package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
  76. package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
  77. package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
  78. package/src/cognition/faculties/reputation.tracker.ts +6 -6
  79. package/src/cognition/faculties/reward.evaluator.ts +4 -4
  80. package/src/cognition/faculties/self.model.updater.ts +8 -8
  81. package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
  83. package/src/cognition/faculties/social.perception.ts +3 -3
  84. package/src/cognition/faculties/spaced.repetition.ts +2 -2
  85. package/src/cognition/faculties/stress.regulator.ts +27 -4
  86. package/src/cognition/faculties/task.switcher.ts +11 -6
  87. package/src/cognition/faculties/theory.of.mind.ts +6 -6
  88. package/src/cognition/faculties/threat.evaluator.ts +3 -3
  89. package/src/cognition/faculties/working.memory.ts +8 -8
  90. package/src/cognition/generative.model.ts +2 -2
  91. package/src/cognition/memory/vector.adapter.ts +5 -5
  92. package/src/cognition/memory/vector.content.ts +4 -4
  93. package/src/cognition/memory/vector.embedder.ts +9 -9
  94. package/src/cognition/persona.prior.ts +1 -1
  95. package/src/cognition/schema.registry.ts +2 -2
  96. package/src/cognition/senses/audition.engine/engine.ts +2 -2
  97. package/src/cognition/senses/base.sense.engine.ts +1 -1
  98. package/src/cognition/utilities/token.tracker.ts +11 -5
  99. package/src/core/abstracts.ts +1 -1
  100. package/src/core/completion.recorder.ts +2 -2
  101. package/src/core/serialization.ts +3 -3
  102. package/src/core/snapshot.manager.ts +1 -1
  103. package/src/core/state.manager.ts +1 -1
  104. package/src/core/utils.ts +1 -1
  105. package/src/host/boot.ts +59 -37
  106. package/src/host/utterances.ts +1 -1
  107. package/src/llm/index.ts +84 -34
  108. package/src/llm/summarizer.ts +4 -4
  109. package/src/llm/wire.contracts.ts +1 -1
  110. package/src/mcp/effectors.ts +10 -10
  111. package/src/mcp/server.ts +11 -11
  112. package/src/pma/eval.ts +1 -1
  113. package/src/pma/index.ts +21 -21
  114. package/src/runners/outreach.runner.ts +3 -3
  115. package/src/runners/social.runner.ts +1 -1
  116. package/src/runners/thin-shim.runner.ts +1 -1
  117. package/src/sdk/will.ts +20 -11
  118. package/src/serve/server.ts +12 -12
  119. package/src/stem/assembly.audit.ts +3 -3
  120. package/src/stem/guards/identity.coherence.ts +13 -12
  121. package/src/stem/guards/identity.guard.ts +22 -22
  122. package/src/stem/index.ts +11 -11
  123. package/src/stem/mind.ts +12 -12
  124. package/src/stem/tracts/biography.writer.ts +3 -3
  125. package/src/stem/tracts/effector.controller.ts +4 -4
  126. package/src/stem/tracts/health.reporter.ts +1 -1
  127. package/src/stem/tracts/outbox.controller.ts +4 -0
  128. package/src/stem/tracts/outbox.writer.ts +4 -4
  129. package/src/stem/tracts/pma.controller.ts +1 -1
  130. package/src/stem/tracts/replay.controller.ts +10 -10
  131. package/src/stem/tracts/sensory.controller.ts +2 -2
  132. package/src/stem/tracts/session.logger.ts +4 -4
  133. package/src/stem/tracts/transport/socketio.transport.ts +7 -7
  134. package/src/stem/tracts/transport/stream.transport.ts +2 -2
  135. package/src/stem/tracts/transport.controller.ts +15 -15
package/README.md CHANGED
@@ -19,7 +19,7 @@ Regulatory → Perceptual → Affective → Memory → Executive → Meta-cognit
19
19
 
20
20
  The mind is not re-derived from a prompt each run. It **accretes**: traits develop from experience, beliefs consolidate, skills proceduralise, and a coherent self carries across restarts as a portable, eval-verified artifact.
21
21
 
22
- Embed it five ways — [**SDK facade**](#the-will-sdk-facade--recommended) (Node/TS) · [**Discord**](#a-will-in-your-discord-server--will-discord) (a mind in your server) · [**Claude Desktop via MCP**](#the-mcp-server--a-persistent-mind-in-claude-desktop--claude-code) · [**MCP tools as its abilities**](#employing-mcp-tools--the-mind-gets-abilities) · [**HTTP sidecar / Docker**](#the-http-sidecar--will-serve-any-language-or-docker) (any language). [Pick a surface →](#use-it-in-your-project)
22
+ Embed it six ways — [**SDK facade**](#the-will-sdk-facade--recommended) (Node/TS) · [**Discord**](#a-will-in-your-discord-server--will-discord) (a mind in your server) · [**WhatsApp**](docs/channels/whatsapp.md) (QR-pair a linked device) · [**Claude Desktop via MCP**](#the-mcp-server--a-persistent-mind-in-claude-desktop--claude-code) · [**MCP tools as its abilities**](#employing-mcp-tools--the-mind-gets-abilities) · [**HTTP sidecar / Docker**](#the-http-sidecar--will-serve-any-language-or-docker) (any language). [Pick a surface →](#use-it-in-your-project)
23
23
 
24
24
  ---
25
25
 
@@ -84,12 +84,13 @@ ticks — or earlier when physiology demands it.
84
84
 
85
85
  ## Use it in your project
86
86
 
87
- Runs anywhere **Node 18+ or Bun** runs (the engine is Node-compatible; Bun is the primary target). **Five surfaces**, one paradigm — you *perceive* things to a mind and *observe what it projects*; it may act, speak, or stay silent, and it persists across restarts via its [PMA artifact](#pma--the-persistent-mind-artifact):
87
+ Runs anywhere **Node 18+ or Bun** runs (the engine is Node-compatible; Bun is the primary target). **Six surfaces**, one paradigm — you *perceive* things to a mind and *observe what it projects*; it may act, speak, or stay silent, and it persists across restarts via its [PMA artifact](#pma--the-persistent-mind-artifact):
88
88
 
89
89
  | Surface | You are… | Start with |
90
90
  |---|---|---|
91
91
  | [**SDK facade**](#the-will-sdk-facade--recommended) | a Node/TypeScript app embedding a mind | `import { Will } from '@mindot/will'` |
92
92
  | [**Discord**](#a-will-in-your-discord-server--will-discord) | a server where a mind should *live* | `npx -y @mindot/will discord` |
93
+ | [**WhatsApp**](docs/channels/whatsapp.md) | the chats people actually check — via a QR-paired linked device (unofficial; read the guide's warning) | `npx -y @mindot/will whatsapp` |
93
94
  | [**MCP host**](#the-mcp-server--a-persistent-mind-in-claude-desktop--claude-code) | Claude Desktop / Claude Code / an IDE | `npx -y @mindot/will mcp` |
94
95
  | [**MCP tools as abilities**](#employing-mcp-tools--the-mind-gets-abilities) | giving the mind tools it *chooses* to use | `import { connectMcpEffectors } from '@mindot/will/mcp'` |
95
96
  | [**HTTP sidecar**](#the-http-sidecar--will-serve-any-language-or-docker) | Python, Go, a game server — any language, or Docker | `npx -y @mindot/will serve` |
@@ -778,11 +779,12 @@ cd will && bun run build
778
779
 
779
780
  | Variable | Default | Description |
780
781
  |---|---|---|
781
- | `WILL_LLM_PROVIDER` | `anthropic` | `anthropic` supported today; `openai` · `deepseek` · `google` scaffolded |
782
- | `WILL_LLM_MODEL` | *(model default)* | Model name for the chosen provider |
783
- | `WILL_LLM_API_KEY` | — | API key for the chosen provider. Falls back to `ANTHROPIC_API_KEY` |
784
- | `WILL_LLM_BASE_URL` | *(provider default)* | Override the provider API base URL (e.g. `http://localhost:11434/v1`). Falls back to `OPENAI_BASE_URL` |
785
- | `WILL_LLM_TIMEOUT_MS` | `90000` | LLM timeout. On Anthropic (streaming) this is a *first-byte*/TTFT deadline long completions aren't aborted mid-generation |
782
+ | `WILL_LLM_PROVIDER` | `anthropic` | `anthropic` · `glm` supported today; `openai` · `deepseek` · `google` scaffolded |
783
+ | `WILL_LLM_MODEL` | *(provider default)* | Model id for the chosen provider (`claude-sonnet-4-5-20250929` / `glm-5.2`) |
784
+ | `WILL_LLM_API_KEY` | — | API key for the chosen provider. Falls back to `ANTHROPIC_API_KEY` (or `ZAI_API_KEY` on `glm`) |
785
+ | `ZAI_API_KEY` | | Z.ai key. Its presence alone selects the `glm` provider when `WILL_LLM` is unset |
786
+ | `WILL_LLM_BASE_URL` | *(provider default)* | Override the provider API base URL (e.g. a self-hosted GLM at `http://localhost:8000/anthropic`). Falls back to `OPENAI_BASE_URL` |
787
+ | `WILL_LLM_TIMEOUT_MS` | `90000` | LLM timeout. On the Anthropic-wire providers (`anthropic`, `glm` — both streaming) this is a *first-byte*/TTFT deadline — long completions aren't aborted mid-generation |
786
788
  | `WILL_LLM_CONCURRENCY` | `3` | Max concurrent LLM calls (min 3: executive + conversation + summary) |
787
789
  | `WILL_TICK_MS` | `1000` | Milliseconds between ticks |
788
790
  | `WILL_MAX_TICKS` | `0` | Stop after N ticks. `0` = run forever |
@@ -804,18 +806,36 @@ cd will && bun run build
804
806
  | `WILL_DISCORD_CHANNELS` | *(all visible)* | Comma-separated channel ids the Will inhabits |
805
807
  | `WILL_DISCORD_MENTION_ONLY` | `false` | Perceive guild messages only when @mentioned (DMs always perceived) |
806
808
  | `WILL_DISCORD_HOME_CHANNEL` | — | Fallback channel for utterances with no reachable addressee |
809
+ | `WILL_WHATSAPP_CHATS` | *(all chats)* | Comma-separated chat jids the Will inhabits (`will whatsapp` — [guide + warning](docs/channels/whatsapp.md)) |
810
+ | `WILL_WHATSAPP_MENTION_ONLY` | `false` | Perceive group messages only when @mentioned (DMs always perceived) |
811
+ | `WILL_WHATSAPP_HOME_CHAT` | — | Fallback chat jid for utterances with no reachable addressee |
807
812
  | `OPENAI_BASE_URL` | — | Base URL override for local / OpenAI-compatible models |
808
813
 
809
814
  ---
810
815
 
811
816
  ## LLM provider
812
817
 
813
- **Anthropic is the supported provider today** — the only one with full streaming + structured-output support and the only one currently exercised in production.
818
+ **Two providers are supported today** — Anthropic and Z.ai's GLM. Both speak the Anthropic Messages wire, so both get the full path: token streaming, the first-byte (TTFT) deadline, prompt-cache breakpoints, and the structured-output contract.
814
819
 
815
- | Provider | `WILL_LLM_PROVIDER` | Status |
816
- |---|---|---|
817
- | Anthropic | `anthropic` | ✅ Supported — streaming, structured output |
818
- | OpenAI · DeepSeek · Google | `openai` · `deepseek` · `google` | ⚠️ Scaffoldedcode paths exist (non-streaming), not yet production-ready |
820
+ | Provider | `WILL_LLM_PROVIDER` | Key | Status |
821
+ |---|---|---|---|
822
+ | Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | ✅ Supported — streaming, structured output |
823
+ | **Z.ai GLM** | `glm` | `ZAI_API_KEY` | Supportedstreaming, structured output (via Z.ai's Anthropic-compatible endpoint) |
824
+ | OpenAI · DeepSeek · Google | `openai` · `deepseek` · `google` | `WILL_LLM_API_KEY` | ⚠️ Scaffolded — code paths exist (non-streaming), not yet production-ready |
825
+
826
+ ### Running a mind on GLM
827
+
828
+ ```bash
829
+ ZAI_API_KEY=… WILL_NAME=Aria npx -y @mindot/will discord
830
+ ```
831
+
832
+ That's all — the mode auto-detects from the key present, `WILL_LLM_PROVIDER=glm` defaults to `https://api.z.ai/api/anthropic`, and the model defaults to `glm-5.2`. Pin the 1M-context variant with `WILL_LLM_MODEL=glm-5.2[1m]`.
833
+
834
+ Why it matters for a Will specifically: a mind is **always on**. It reasons every N ticks whether or not anyone spoke, so the executive is a standing cost, not a per-request one — the arithmetic that makes a cheaper capable model matter more here than in a request/response agent. GLM-5.2 runs about **$1.40 / $4.40** per Mtok against Claude Sonnet's $3 / $15, with a 1M context.
835
+
836
+ `WILL_LLM_BASE_URL` points `glm` at any **Anthropic-compatible** endpoint — a gateway (LiteLLM, claude-code-router), or one fronting the open weights (GLM-5.2 is MIT-licensed). Note the wire, not just the model: vLLM/SGLang serve an *OpenAI*-shaped API, so a bare self-host belongs on `WILL_LLM_PROVIDER=openai` — today's scaffold, no streaming — until an Anthropic-compatible shim sits in front.
837
+
838
+ A Will's identity survives the swap either way: the [PMA](#pma--the-persistent-mind-artifact) carries the self across a model change, and `runPMAEval` scores how faithfully — continuity across providers is measurable, not asserted.
819
839
 
820
840
  The provider layer is an in-house `fetch` client (`src/llm/index.ts`) with a global concurrency gate (`src/llm/gate.ts`) — no Mastra / ai-sdk runtime dependency.
821
841
 
@@ -1,14 +1,5 @@
1
- import { W as Will } from '../will-D-slky1N.js';
2
-
3
- /** A running connection between one Will and one platform. */
4
- interface ChannelBridge {
5
- /** Platform kind, e.g. 'discord'. */
6
- readonly kind: string;
7
- /** Connect and start relaying. Resolves once the bridge is live. */
8
- start(): Promise<void>;
9
- /** Disconnect and release resources. Idempotent. */
10
- close(): Promise<void>;
11
- }
1
+ import { W as Will } from '../will-Bikuk4s2.js';
2
+ import { C as ChannelBridge } from '../types-E9-HV-SW.js';
12
3
 
13
4
  interface DiscordLikeChannel {
14
5
  send(content: string): Promise<unknown>;
@@ -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/** 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"]}
@@ -0,0 +1,72 @@
1
+ import { W as Will } from '../will-Bikuk4s2.js';
2
+ import { C as ChannelBridge } from '../types-E9-HV-SW.js';
3
+
4
+ interface WaLikeMessage {
5
+ key: {
6
+ remoteJid?: string | null;
7
+ fromMe?: boolean | null;
8
+ /** Sender jid inside a group (absent in DMs). */
9
+ participant?: string | null;
10
+ };
11
+ /** Sender's display ("push") name. */
12
+ pushName?: string | null;
13
+ messageStubType?: number | null;
14
+ message?: {
15
+ conversation?: string | null;
16
+ extendedTextMessage?: {
17
+ text?: string | null;
18
+ contextInfo?: {
19
+ mentionedJid?: string[] | null;
20
+ } | null;
21
+ } | null;
22
+ imageMessage?: {
23
+ caption?: string | null;
24
+ } | null;
25
+ videoMessage?: {
26
+ caption?: string | null;
27
+ } | null;
28
+ } | null;
29
+ }
30
+ interface WaLikeSocket {
31
+ /** The paired account, once connected. id like '4915…:12@s.whatsapp.net'. */
32
+ user?: {
33
+ id: string;
34
+ name?: string;
35
+ } | null;
36
+ ev: {
37
+ on(event: 'messages.upsert', fn: (u: {
38
+ messages: WaLikeMessage[];
39
+ type?: string;
40
+ }) => void): unknown;
41
+ };
42
+ sendMessage(jid: string, content: {
43
+ text: string;
44
+ }): Promise<unknown>;
45
+ sendPresenceUpdate?(state: 'composing' | 'paused', jid?: string): Promise<unknown>;
46
+ /** Tear the connection down (does not unlink the device). */
47
+ end?(err?: Error): void;
48
+ }
49
+ interface WhatsAppBridgeOptions {
50
+ /** Chat jids the Will inhabits (groups `…@g.us`, DMs `…@s.whatsapp.net`). Unset = every chat. */
51
+ chats?: string[];
52
+ /** Perceive group messages only when the Will is @mentioned (DMs always perceived). */
53
+ mentionOnly?: boolean;
54
+ /** Fallback chat jid for utterances with no reachable addressee. */
55
+ homeChatId?: string;
56
+ /** Linked-device credentials dir (default: ./.will/<willId>.wa-auth). QR pairs on first run. */
57
+ authPath?: string;
58
+ /** Roster path (default: ./.will/<willId>.whatsapp.json). */
59
+ rosterPath?: string;
60
+ /** Test / power-user seam: bring your own socket; Baileys is never imported. */
61
+ socket?: WaLikeSocket;
62
+ log?: (msg: string) => void;
63
+ }
64
+ /**
65
+ * Connect a Will to WhatsApp over the linked-device protocol. First run prints
66
+ * a QR to pair (Settings → Linked devices → Link a device); credentials persist
67
+ * in `authPath` so later runs reconnect silently. Close via the returned
68
+ * `ChannelBridge.close()` — the Will keeps ticking; it only loses this surface.
69
+ */
70
+ declare function connectWhatsApp(will: Will, opts?: WhatsAppBridgeOptions): Promise<ChannelBridge>;
71
+
72
+ export { type WaLikeMessage, type WaLikeSocket, type WhatsAppBridgeOptions, connectWhatsApp };
@@ -0,0 +1,252 @@
1
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ // src/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/channels/types.ts
59
+ function chunkText(text, max) {
60
+ if (text.length <= max) return [text];
61
+ const chunks = [];
62
+ let rest = text;
63
+ while (rest.length > max) {
64
+ const window = rest.slice(0, max);
65
+ const cut = Math.max(window.lastIndexOf("\n\n"), window.lastIndexOf("\n"), window.lastIndexOf(" "));
66
+ const at = cut > max * 0.5 ? cut : max;
67
+ chunks.push(rest.slice(0, at).trimEnd());
68
+ rest = rest.slice(at).trimStart();
69
+ }
70
+ if (rest) chunks.push(rest);
71
+ return chunks;
72
+ }
73
+
74
+ // src/channels/whatsapp.ts
75
+ var WHATSAPP_MESSAGE_LIMIT = 65536;
76
+ var isGroupJid = (jid) => jid.endsWith("@g.us");
77
+ var bareId = (jid) => jid.split("@")[0].split(":")[0];
78
+ var dmJidFor = (userId) => `${userId}@s.whatsapp.net`;
79
+ function textOf(m) {
80
+ const msg = m.message;
81
+ return msg?.conversation ?? msg?.extendedTextMessage?.text ?? msg?.imageMessage?.caption ?? msg?.videoMessage?.caption ?? "";
82
+ }
83
+ async function connectWhatsApp(will, opts = {}) {
84
+ const log = opts.log ?? ((m) => console.error(`[will:whatsapp] ${m}`));
85
+ const roster = new ChannelRoster(opts.rosterPath ?? `.will/${will.id}.whatsapp.json`);
86
+ const allowed = opts.chats?.length ? new Set(opts.chats) : null;
87
+ let closed = false;
88
+ const socket = opts.socket ?? await createWhatsAppSocket({
89
+ authPath: opts.authPath ?? `.will/${will.id}.wa-auth`,
90
+ log,
91
+ stillOpen: () => !closed
92
+ });
93
+ let lastActiveChatId = opts.homeChatId ?? null;
94
+ socket.ev.on("messages.upsert", ({ messages, type }) => {
95
+ if (type && type !== "notify") return;
96
+ for (const m of messages) void onMessage(m);
97
+ });
98
+ async function onMessage(m) {
99
+ const jid = m.key.remoteJid;
100
+ if (!jid || m.key.fromMe || !m.message || m.messageStubType) return;
101
+ if (jid.endsWith("@broadcast") || jid.endsWith("@newsletter")) return;
102
+ if (allowed && !allowed.has(jid)) return;
103
+ const isGroup = isGroupJid(jid);
104
+ const senderJid = isGroup ? m.key.participant : jid;
105
+ if (!senderJid) return;
106
+ const selfId = socket.user ? bareId(socket.user.id) : null;
107
+ const mentioned = m.message.extendedTextMessage?.contextInfo?.mentionedJid ?? [];
108
+ const addressed = !isGroup || selfId != null && mentioned.some((j) => bareId(j) === selfId);
109
+ if (opts.mentionOnly && !addressed) return;
110
+ const userId = bareId(senderJid);
111
+ const entityId = `whatsapp:${userId}`;
112
+ const speaker = m.pushName ?? void 0;
113
+ roster.record({
114
+ entityId,
115
+ userId,
116
+ ...speaker ? { displayName: speaker } : {},
117
+ ...isGroup ? { lastChannelId: jid } : { dmChannelId: jid }
118
+ });
119
+ if (isGroup) lastActiveChatId = jid;
120
+ if (addressed) await socket.sendPresenceUpdate?.("composing", jid).catch(() => {
121
+ });
122
+ const text = textOf(m);
123
+ if (!text.trim()) return;
124
+ await will.perceive({
125
+ text,
126
+ from: entityId,
127
+ thread: `whatsapp:${jid}`,
128
+ ...speaker ? { speaker } : {}
129
+ });
130
+ }
131
+ will.on("message", (m) => {
132
+ if (!closed) void deliver(m);
133
+ });
134
+ async function deliver(m) {
135
+ const peer = m.to ? roster.resolve(m.to) : void 0;
136
+ const chunks = chunkText(m.content, WHATSAPP_MESSAGE_LIMIT);
137
+ 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];
139
+ for (const jid of targets) {
140
+ if (!jid) continue;
141
+ try {
142
+ for (const chunk of chunks) await socket.sendMessage(jid, { text: chunk });
143
+ return;
144
+ } catch {
145
+ }
146
+ }
147
+ log(`no route for utterance to '${m.to}' \u2014 dropped (${m.content.length} chars)`);
148
+ }
149
+ return {
150
+ kind: "whatsapp",
151
+ async start() {
152
+ log(`${will.name} is present on WhatsApp${socket.user ? ` as ${bareId(socket.user.id)}` : ""}`);
153
+ },
154
+ async close() {
155
+ if (closed) return;
156
+ closed = true;
157
+ roster.flush();
158
+ try {
159
+ socket.end?.();
160
+ } catch {
161
+ }
162
+ }
163
+ };
164
+ }
165
+ async function createWhatsAppSocket(o) {
166
+ let baileys;
167
+ try {
168
+ baileys = await import('baileys');
169
+ } catch {
170
+ throw new Error("baileys is not installed (it is an optionalDependency) \u2014 run `bun add baileys` / `npm i baileys` and retry.");
171
+ }
172
+ const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = baileys;
173
+ const noop = () => {
174
+ };
175
+ const logger = {
176
+ level: "silent",
177
+ child() {
178
+ return logger;
179
+ },
180
+ trace: noop,
181
+ debug: noop,
182
+ info: noop,
183
+ warn: noop,
184
+ error: noop,
185
+ fatal: noop
186
+ };
187
+ const subscribers = [];
188
+ let inner = null;
189
+ const facade = {
190
+ get user() {
191
+ return inner?.user ? { id: inner.user.id, name: inner.user.name ?? void 0 } : null;
192
+ },
193
+ ev: { on: (_e, fn) => {
194
+ subscribers.push(fn);
195
+ } },
196
+ sendMessage: (jid, content) => {
197
+ if (!inner) return Promise.reject(new Error("whatsapp socket not connected"));
198
+ return inner.sendMessage(jid, content);
199
+ },
200
+ sendPresenceUpdate: (state2, jid) => inner?.sendPresenceUpdate(state2, jid) ?? Promise.resolve(),
201
+ end: () => inner?.end(void 0)
202
+ };
203
+ const { state, saveCreds } = await useMultiFileAuthState(o.authPath);
204
+ await new Promise((resolveOpen, rejectOpen) => {
205
+ let opened = false;
206
+ function connect() {
207
+ const sock = makeWASocket({ auth: state, logger });
208
+ inner = sock;
209
+ sock.ev.on("creds.update", saveCreds);
210
+ sock.ev.on("messages.upsert", (u) => {
211
+ for (const fn of subscribers) fn(u);
212
+ });
213
+ sock.ev.on("connection.update", (update) => {
214
+ const { connection, lastDisconnect, qr } = update;
215
+ if (qr) void printQr(qr, o.log);
216
+ if (connection === "open" && !opened) {
217
+ opened = true;
218
+ resolveOpen();
219
+ }
220
+ if (connection === "close") {
221
+ const code = lastDisconnect?.error?.output?.statusCode;
222
+ if (code === DisconnectReason.loggedOut) {
223
+ const err = new Error("WhatsApp unlinked this device (logged out) \u2014 delete the auth dir and pair again.");
224
+ o.log(err.message);
225
+ if (!opened) rejectOpen(err);
226
+ return;
227
+ }
228
+ if (o.stillOpen()) {
229
+ o.log(`connection closed (status ${code ?? "?"}) \u2014 reconnecting\u2026`);
230
+ setTimeout(connect, 3e3);
231
+ }
232
+ }
233
+ });
234
+ }
235
+ connect();
236
+ });
237
+ return facade;
238
+ }
239
+ async function printQr(qr, log) {
240
+ log("pair this device: WhatsApp \u2192 Settings \u2192 Linked devices \u2192 Link a device");
241
+ try {
242
+ const qrt = await import('qrcode-terminal');
243
+ (qrt.default ?? qrt).generate(qr, { small: true });
244
+ } catch {
245
+ log(`qrcode-terminal not installed \u2014 raw pairing code:
246
+ ${qr}`);
247
+ }
248
+ }
249
+
250
+ export { connectWhatsApp };
251
+ //# sourceMappingURL=whatsapp.js.map
252
+ //# sourceMappingURL=whatsapp.js.map
@@ -0,0 +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"]}