@mindot/will 0.3.0 → 0.5.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 (57) hide show
  1. package/README.md +63 -21
  2. package/dist/channels/discord.d.ts +69 -0
  3. package/dist/channels/discord.js +193 -0
  4. package/dist/channels/discord.js.map +1 -0
  5. package/dist/channels/whatsapp.d.ts +72 -0
  6. package/dist/channels/whatsapp.js +252 -0
  7. package/dist/channels/whatsapp.js.map +1 -0
  8. package/dist/cli.js +904 -356
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +141 -141
  11. package/dist/index.js +441 -342
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/types-E9-HV-SW.d.ts +11 -0
  15. package/dist/{will-B5eKs3Wv.d.ts → will-BDq-TMQr.d.ts} +4013 -3916
  16. package/package.json +17 -3
  17. package/src/channels/discord.ts +214 -0
  18. package/src/channels/roster.ts +87 -0
  19. package/src/channels/types.ts +46 -0
  20. package/src/channels/whatsapp.ts +318 -0
  21. package/src/cli.ts +57 -9
  22. package/src/cognition/agency/engines/deliberation.engine.ts +7 -7
  23. package/src/cognition/agency/execution.primitives.ts +11 -11
  24. package/src/cognition/agency/proactive.communicator.ts +8 -8
  25. package/src/cognition/config.mirror.entities.ts +2 -2
  26. package/src/cognition/conversation.memory.ts +1 -1
  27. package/src/cognition/faculties/executive.engine/commands.ts +7 -15
  28. package/src/cognition/faculties/executive.engine/engine.ts +66 -35
  29. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +1 -1
  30. package/src/cognition/faculties/executive.engine/facet.ts +1 -1
  31. package/src/cognition/faculties/executive.engine/prompt.factory.ts +76 -61
  32. package/src/cognition/faculties/executive.engine/types.ts +1 -1
  33. package/src/cognition/faculties/introspection.engine.ts +1 -2
  34. package/src/cognition/faculties/planning.engine/engine.ts +38 -1
  35. package/src/cognition/faculties/planning.engine/plan.store.ts +42 -0
  36. package/src/cognition/faculties/planning.engine/plan.supervision.ts +7 -7
  37. package/src/cognition/faculties/theory.of.mind.ts +2 -2
  38. package/src/cognition/senses/audition.engine/engine.ts +21 -21
  39. package/src/cognition/utilities/token.tracker.ts +6 -0
  40. package/src/host/boot.ts +95 -7
  41. package/src/llm/index.ts +75 -25
  42. package/src/llm/summarizer.ts +2 -2
  43. package/src/profiles/companion.ts +14 -14
  44. package/src/profiles/company-brain.ts +19 -19
  45. package/src/profiles/customer-service.ts +17 -17
  46. package/src/profiles/game-npc.ts +10 -10
  47. package/src/profiles/index.ts +2 -2
  48. package/src/profiles/smart-home.ts +16 -16
  49. package/src/runners/outreach.runner.ts +6 -9
  50. package/src/runners/social.runner.ts +1 -4
  51. package/src/runners/thin-shim.runner.ts +4 -6
  52. package/src/sdk/will.ts +42 -16
  53. package/src/stem/guards/identity.coherence.ts +9 -6
  54. package/src/stem/guards/identity.guard.ts +20 -9
  55. package/src/stem/index.ts +7 -7
  56. package/src/stem/mind.ts +182 -98
  57. package/src/stem/tracts/outbox.controller.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindot/will",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "author": "Fabrice <fabrice8@github.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,9 @@
21
21
  "zod": "^4.0.0"
22
22
  },
23
23
  "optionalDependencies": {
24
+ "baileys": "^7.0.0-rc13",
25
+ "discord.js": "^14.16.3",
26
+ "qrcode-terminal": "^0.12.0",
24
27
  "socket.io-client": "^4.8.1"
25
28
  },
26
29
  "exports": {
@@ -31,16 +34,27 @@
31
34
  "./mcp": {
32
35
  "import": "./dist/mcp/effectors.js",
33
36
  "types": "./dist/mcp/effectors.d.ts"
37
+ },
38
+ "./discord": {
39
+ "import": "./dist/channels/discord.js",
40
+ "types": "./dist/channels/discord.d.ts"
41
+ },
42
+ "./whatsapp": {
43
+ "import": "./dist/channels/whatsapp.js",
44
+ "types": "./dist/channels/whatsapp.d.ts"
34
45
  }
35
46
  },
36
47
  "bugs": {
37
48
  "url": "https://github.com/mindot-ai/will/issues"
38
49
  },
39
- "description": "Will an engine for persistent machine minds. A Will is a continuously-ticking synthetic mind built from 38 cognitive faculties, a learning agency pipeline, and a dual-process executive deterministic, persistent, and portable (PMA).",
50
+ "description": "Will \u2014 an engine for persistent machine minds. A Will is a continuously-ticking synthetic mind built from 38 cognitive faculties, a learning agency pipeline, and a dual-process executive \u2014 deterministic, persistent, and portable (PMA).",
40
51
  "engines": {
41
52
  "bun": ">=1.0.0"
42
53
  },
43
- "files": ["dist", "src"],
54
+ "files": [
55
+ "dist",
56
+ "src"
57
+ ],
44
58
  "homepage": "https://github.com/mindot-ai/will#readme",
45
59
  "keywords": [
46
60
  "ai",
@@ -0,0 +1,214 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/channels/discord.ts — a Will present in a Discord server
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // The bridge relays both directions of the paradigm and nothing else:
6
+ //
7
+ // inbound guild/DM message → will.perceive({ from, speaker, text, thread })
8
+ // — every author is `discord:<userId>` (stable across guilds), the
9
+ // display name is *learned* by the mind, and each Discord channel
10
+ // is its own conversation thread.
11
+ // outbound will.on('message') → the addressee's last shared channel, else
12
+ // their DM, else the home channel. Proactive utterances (the mind
13
+ // speaking first) route the same way — that is the point.
14
+ //
15
+ // The Will decides when to speak. There is no command prefix and no forced
16
+ // reply: unaddressed chatter is perceived (salience-scored by audition) and
17
+ // silence is a valid outcome. `mentionOnly` narrows perception for busy
18
+ // servers; it does not turn the bridge into an ask() surface.
19
+ //
20
+ // discord.js is imported lazily inside `createDiscordClient` — tests (and any
21
+ // host that brings its own client) inject `client`, and the structural
22
+ // `DiscordLikeClient` type keeps the dependency out of the type graph.
23
+ // ─────────────────────────────────────────────────────────────
24
+
25
+ import type { Will, WillMessage } from '#sdk/will'
26
+ import { ChannelRoster } from '#channels/roster'
27
+ import { chunkText, type ChannelBridge } from '#channels/types'
28
+
29
+ const DISCORD_MESSAGE_LIMIT = 2000
30
+
31
+ // ── The slice of discord.js the bridge actually uses (structural) ───────────
32
+
33
+ export interface DiscordLikeChannel {
34
+ send( content: string ): Promise<unknown>
35
+ sendTyping?(): Promise<unknown>
36
+ }
37
+
38
+ export interface DiscordLikeMessage {
39
+ content: string
40
+ cleanContent?: string
41
+ channelId: string
42
+ guildId?: string | null
43
+ author: { id: string; bot?: boolean; username?: string; displayName?: string }
44
+ member?: { displayName?: string } | null
45
+ mentions?: { has( userId: string ): boolean }
46
+ channel: DiscordLikeChannel
47
+ }
48
+
49
+ export interface DiscordLikeClient {
50
+ user: { id: string; setPresence?( p: unknown ): void } | null
51
+ /** discord.js ≥14.22; polled so we needn't subscribe to the deprecated `ready`. */
52
+ isReady?(): boolean
53
+ on( event: 'messageCreate', fn: ( m: DiscordLikeMessage ) => void ): unknown
54
+ once( event: string, fn: () => void ): unknown
55
+ login( token: string ): Promise<unknown>
56
+ destroy(): Promise<unknown> | void
57
+ channels: { fetch( id: string ): Promise<unknown> }
58
+ users: { fetch( id: string ): Promise<{ send( content: string ): Promise<unknown> }> }
59
+ }
60
+
61
+ // ── Options ──────────────────────────────────────────────────────────────────
62
+
63
+ export interface DiscordBridgeOptions {
64
+ /** Bot token (Discord developer portal). Unused when `client` is injected pre-logged-in. */
65
+ token?: string
66
+ /** Channel ids the Will inhabits. Unset = every channel it can see. */
67
+ channels?: string[]
68
+ /** Perceive guild messages only when the Will is @mentioned (DMs always perceived). */
69
+ mentionOnly?: boolean
70
+ /** Fallback channel for utterances with no reachable addressee. */
71
+ homeChannelId?: string
72
+ /** Roster path (default: ./.will/<willId>.discord.json). */
73
+ rosterPath?: string
74
+ /** Test / power-user seam: bring your own client; discord.js is never imported. */
75
+ client?: DiscordLikeClient
76
+ log?: ( msg: string ) => void
77
+ }
78
+
79
+ // ── The bridge ───────────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Connect a Will to Discord. Resolves once the bridge is live (logged in and
83
+ * relaying). Close it via the returned `ChannelBridge.close()` — the Will
84
+ * itself is not stopped; it simply loses this surface.
85
+ */
86
+ export async function connectDiscord( will: Will, opts: DiscordBridgeOptions ): Promise<ChannelBridge> {
87
+ const log = opts.log ?? ( ( m: string ) => console.error( `[will:discord] ${ m }` ) )
88
+ const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.discord.json` )
89
+ const allowed = opts.channels?.length ? new Set( opts.channels ) : null
90
+
91
+ const client = opts.client ?? await createDiscordClient()
92
+
93
+ /** The most recently active allowed channel — last-resort proactive target. */
94
+ let lastActiveChannelId: string | null = opts.homeChannelId ?? null
95
+
96
+ // ── inbound: platform message → stimulus ──────────────────────────────────
97
+ client.on( 'messageCreate', message => { void onMessage( message ) } )
98
+
99
+ async function onMessage( message: DiscordLikeMessage ): Promise<void> {
100
+ const self = client.user
101
+ if( !self || message.author.id === self.id || message.author.bot ) return
102
+
103
+ const isDM = !message.guildId
104
+ if( !isDM && allowed && !allowed.has( message.channelId ) ) return
105
+
106
+ const addressed = isDM || ( message.mentions?.has( self.id ) ?? false )
107
+ if( opts.mentionOnly && !addressed ) return
108
+
109
+ const entityId = `discord:${ message.author.id }`
110
+ const speaker = message.member?.displayName ?? message.author.displayName ?? message.author.username
111
+
112
+ roster.record( {
113
+ entityId,
114
+ userId: message.author.id,
115
+ ...( speaker ? { displayName: speaker } : {} ),
116
+ ...( isDM ? { dmChannelId: message.channelId } : { lastChannelId: message.channelId } ),
117
+ } )
118
+ if( !isDM ) lastActiveChannelId = message.channelId
119
+
120
+ // Being addressed is the one moment a presence cue is honest — the mind
121
+ // may still choose silence, and typing expires on its own.
122
+ if( addressed ) await message.channel.sendTyping?.().catch( () => {} )
123
+
124
+ const text = message.cleanContent || message.content
125
+ if( !text.trim() ) return
126
+
127
+ await will.perceive( {
128
+ text,
129
+ from: entityId,
130
+ thread: `discord:${ message.channelId }`,
131
+ ...( speaker ? { speaker } : {} ),
132
+ } )
133
+ }
134
+
135
+ // ── outbound: projected utterance → the addressee ─────────────────────────
136
+ // The facade has no off(); the bridge gates its handler on `closed` instead.
137
+ let closed = false
138
+ will.on( 'message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )
139
+
140
+ async function deliver( m: WillMessage ): Promise<void> {
141
+ const peer = m.to ? roster.resolve( m.to ) : undefined
142
+ const chunks = chunkText( m.content, DISCORD_MESSAGE_LIMIT )
143
+
144
+ // Preference order: where we last shared a room → their DM → home channel.
145
+ const channelIds = [ peer?.lastChannelId, peer?.dmChannelId, opts.homeChannelId ?? undefined, lastActiveChannelId ?? undefined ]
146
+ for( const id of channelIds ){
147
+ if( !id ) continue
148
+ try {
149
+ const channel = await client.channels.fetch( id ) as DiscordLikeChannel | null
150
+ if( !channel?.send ) continue
151
+ for( const chunk of chunks ) await channel.send( chunk )
152
+ return
153
+ }
154
+ catch { /* try the next route */ }
155
+ }
156
+ if( peer ){
157
+ try {
158
+ const user = await client.users.fetch( peer.userId )
159
+ for( const chunk of chunks ) await user.send( chunk )
160
+ return
161
+ }
162
+ catch { /* fall through */ }
163
+ }
164
+ log( `no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)` )
165
+ }
166
+
167
+ // ── lifecycle ──────────────────────────────────────────────────────────────
168
+ const bridge: ChannelBridge = {
169
+ kind: 'discord',
170
+ async start(): Promise<void> {
171
+ if( !client.user ){
172
+ // discord.js ≥14.22 renamed `ready` → `clientReady`. Subscribing to the
173
+ // old name is what triggers its DeprecationWarning, so we take the new
174
+ // name and poll `isReady()` for older builds rather than listening.
175
+ const ready = new Promise<void>( resolve => {
176
+ let poll: ReturnType<typeof setInterval> | null = null
177
+ const done = (): void => { if( poll ) clearInterval( poll ); resolve() }
178
+ client.once( 'clientReady', done )
179
+ poll = setInterval( () => { if( client.isReady?.() ) done() }, 100 )
180
+ poll.unref?.()
181
+ } )
182
+ await client.login( opts.token ?? '' )
183
+ await ready
184
+ }
185
+ log( `${ will.name } is present on Discord as user ${ client.user?.id }` )
186
+ },
187
+ async close(): Promise<void> {
188
+ if( closed ) return
189
+ closed = true
190
+ roster.flush()
191
+ await Promise.resolve( client.destroy() ).catch( () => {} )
192
+ },
193
+ }
194
+ return bridge
195
+ }
196
+
197
+ /** Build a real discord.js client (lazy import keeps it out of non-Discord hosts). */
198
+ async function createDiscordClient(): Promise<DiscordLikeClient> {
199
+ let mod: typeof import( 'discord.js' )
200
+ try { mod = await import( 'discord.js' ) }
201
+ catch {
202
+ throw new Error( 'discord.js is not installed (it is an optionalDependency) — run `bun add discord.js` / `npm i discord.js` and retry.' )
203
+ }
204
+ const { Client, GatewayIntentBits, Partials } = mod
205
+ return new Client( {
206
+ intents: [
207
+ GatewayIntentBits.Guilds,
208
+ GatewayIntentBits.GuildMessages,
209
+ GatewayIntentBits.MessageContent,
210
+ GatewayIntentBits.DirectMessages,
211
+ ],
212
+ partials: [ Partials.Channel ], // DMs arrive on uncached channels
213
+ } ) as unknown as DiscordLikeClient
214
+ }
@@ -0,0 +1,87 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/channels/roster.ts — who the Will knows on a platform, and where
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // The mind knows *entities*; a platform knows user ids and channels. The roster
6
+ // is the durable seam between them: for each entity the Will has met on a
7
+ // channel it records how to reach them again — so a *proactive* utterance
8
+ // (`message.to` from the mind's own initiative) can find its person after a
9
+ // restart, not just within one session.
10
+ //
11
+ // It persists as a small JSON file next to the PMA artifact. Writes are
12
+ // throttled (the file is advisory routing state, not cognition — losing the
13
+ // last few seconds costs a fallback delivery, never memory).
14
+ // ─────────────────────────────────────────────────────────────
15
+
16
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
17
+ import { dirname } from 'node:path'
18
+
19
+ /** How to reach one entity on the platform. */
20
+ export interface RosterEntry {
21
+ /** The mind-side entity id, e.g. 'discord:80351110224678912'. */
22
+ entityId: string
23
+ /** The platform-side user id. */
24
+ userId: string
25
+ /** Last display name seen (advisory — the *learned* name lives in the mind). */
26
+ displayName?: string
27
+ /** DM channel id, once one is known. */
28
+ dmChannelId?: string
29
+ /** Last shared (guild) channel this entity spoke in. */
30
+ lastChannelId?: string
31
+ /** Epoch ms of the last message seen from them. */
32
+ lastSeenAt: number
33
+ }
34
+
35
+ const FLUSH_MS = 2_000
36
+
37
+ export class ChannelRoster {
38
+ private entries = new Map<string, RosterEntry>()
39
+ private dirty = false
40
+ private timer: ReturnType<typeof setTimeout> | null = null
41
+
42
+ constructor( private readonly path: string ) {
43
+ if( existsSync( path ) ){
44
+ try {
45
+ const raw = JSON.parse( readFileSync( path, 'utf8' ) ) as RosterEntry[]
46
+ for( const e of Array.isArray( raw ) ? raw : [] ) this.entries.set( e.entityId, e )
47
+ }
48
+ catch { /* a corrupt roster is not worth failing a boot over — start fresh */ }
49
+ }
50
+ }
51
+
52
+ /** Upsert what we just learned about an entity; schedules a throttled flush. */
53
+ record( update: { entityId: string; userId: string } & Partial<Omit<RosterEntry, 'entityId' | 'userId'>> ): RosterEntry {
54
+ const prev = this.entries.get( update.entityId )
55
+ const next: RosterEntry = {
56
+ lastSeenAt: Date.now(),
57
+ ...prev,
58
+ ...Object.fromEntries( Object.entries( update ).filter( ( [ , v ] ) => v !== undefined ) ) as typeof update,
59
+ }
60
+ this.entries.set( next.entityId, next )
61
+ this.dirty = true
62
+ if( !this.timer ){
63
+ this.timer = setTimeout( () => { this.timer = null; this.flush() }, FLUSH_MS )
64
+ this.timer.unref?.()
65
+ }
66
+ return next
67
+ }
68
+
69
+ resolve( entityId: string ): RosterEntry | undefined {
70
+ return this.entries.get( entityId )
71
+ }
72
+
73
+ all(): RosterEntry[] {
74
+ return [ ...this.entries.values() ]
75
+ }
76
+
77
+ /** Write to disk now (no-op when clean). Called by bridges on close. */
78
+ flush(): void {
79
+ if( !this.dirty ) return
80
+ try {
81
+ mkdirSync( dirname( this.path ), { recursive: true } )
82
+ writeFileSync( this.path, JSON.stringify( this.all(), null, 2 ) )
83
+ this.dirty = false
84
+ }
85
+ catch { /* advisory state — never take the mind down over it */ }
86
+ }
87
+ }
@@ -0,0 +1,46 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/channels/types.ts — the channel-bridge contract
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // A channel bridge puts a Will *in a place where people already are* (Discord,
6
+ // Telegram, Slack, …). It is a host surface, not a cognition surface: it turns
7
+ // platform messages into `perceive` stimuli and delivers the Will's projected
8
+ // utterances back — nothing more. The paradigm survives the crossing:
9
+ //
10
+ // • every platform user is an entity the Will comes to know (`from`),
11
+ // with a *learned* name (`speaker`) — never a placeholder;
12
+ // • every platform channel/DM is a conversation thread (`thread`);
13
+ // • the Will decides when to speak. Silence is a valid outcome, so a
14
+ // bridge never fabricates a reply and never times a message out into
15
+ // an error.
16
+ //
17
+ // Bridges live at the same altitude as the MCP/HTTP hosts (src/mcp, src/serve):
18
+ // they wrap the SDK facade, not the stem.
19
+ // ─────────────────────────────────────────────────────────────
20
+
21
+ /** A running connection between one Will and one platform. */
22
+ export interface ChannelBridge {
23
+ /** Platform kind, e.g. 'discord'. */
24
+ readonly kind: string
25
+ /** Connect and start relaying. Resolves once the bridge is live. */
26
+ start(): Promise<void>
27
+ /** Disconnect and release resources. Idempotent. */
28
+ close(): Promise<void>
29
+ }
30
+
31
+ /** Split a message into platform-sized chunks on natural boundaries. */
32
+ export function chunkText( text: string, max: number ): string[] {
33
+ if( text.length <= max ) return [ text ]
34
+ const chunks: string[] = []
35
+ let rest = text
36
+ while( rest.length > max ){
37
+ // Prefer a paragraph break, then a line break, then a space — else hard-cut.
38
+ const window = rest.slice( 0, max )
39
+ const cut = Math.max( window.lastIndexOf( '\n\n' ), window.lastIndexOf( '\n' ), window.lastIndexOf( ' ' ) )
40
+ const at = cut > max * 0.5 ? cut : max
41
+ chunks.push( rest.slice( 0, at ).trimEnd() )
42
+ rest = rest.slice( at ).trimStart()
43
+ }
44
+ if( rest ) chunks.push( rest )
45
+ return chunks
46
+ }