synabun 2.0.5 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/post-plan.mjs +6 -30
- package/hooks/claude-code/session-start.mjs +1 -4
- package/hooks/claude-code/shared.mjs +112 -75
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/package-processes.js +670 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/runtime-dependencies.js +225 -0
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/browser-bench.d.ts +1 -1
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/browser-bench.js +1 -1
- package/mcp-server/dist/services/browser-bench.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +35 -2
- package/mcp-server/dist/services/categories.d.ts.map +1 -1
- package/mcp-server/dist/services/categories.js +77 -33
- package/mcp-server/dist/services/categories.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/desktop-bench.js +1 -1
- package/mcp-server/dist/services/desktop-bench.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +11 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +35 -14
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/bluesky.js +1 -1
- package/mcp-server/dist/tools/bluesky.js.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.js +1 -1
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-batch.js +1 -1
- package/mcp-server/dist/tools/browser-batch.js.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-interact.js +1 -1
- package/mcp-server/dist/tools/browser-interact.js.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.js +1 -1
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-observe.js +1 -1
- package/mcp-server/dist/tools/browser-observe.js.map +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/card-tools.js +1 -1
- package/mcp-server/dist/tools/card-tools.js.map +1 -1
- package/mcp-server/dist/tools/category.d.ts +1 -1
- package/mcp-server/dist/tools/category.d.ts.map +1 -1
- package/mcp-server/dist/tools/category.js +5 -1
- package/mcp-server/dist/tools/category.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +1 -1
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +1 -1
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/computer.d.ts +1 -1
- package/mcp-server/dist/tools/computer.d.ts.map +1 -1
- package/mcp-server/dist/tools/computer.js +1 -1
- package/mcp-server/dist/tools/computer.js.map +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-channel.js +1 -1
- package/mcp-server/dist/tools/discord-channel.js.map +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-guild.js +1 -1
- package/mcp-server/dist/tools/discord-guild.js.map +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-member.js +1 -1
- package/mcp-server/dist/tools/discord-member.js.map +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-message.js +1 -1
- package/mcp-server/dist/tools/discord-message.js.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-role.js +1 -1
- package/mcp-server/dist/tools/discord-role.js.map +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-thread.js +1 -1
- package/mcp-server/dist/tools/discord-thread.js.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.js +1 -1
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
- package/mcp-server/dist/tools/fb-groups.js +1 -1
- package/mcp-server/dist/tools/fb-groups.js.map +1 -1
- package/mcp-server/dist/tools/forget.d.ts +1 -1
- package/mcp-server/dist/tools/forget.d.ts.map +1 -1
- package/mcp-server/dist/tools/forget.js +1 -1
- package/mcp-server/dist/tools/forget.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +1 -1
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.js +1 -1
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.js +1 -1
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.js +1 -1
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/image-tools.js.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
- package/mcp-server/dist/tools/loop.d.ts +1 -1
- package/mcp-server/dist/tools/loop.d.ts.map +1 -1
- package/mcp-server/dist/tools/loop.js +1 -1
- package/mcp-server/dist/tools/loop.js.map +1 -1
- package/mcp-server/dist/tools/memories.d.ts +1 -1
- package/mcp-server/dist/tools/memories.d.ts.map +1 -1
- package/mcp-server/dist/tools/memories.js +1 -1
- package/mcp-server/dist/tools/memories.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +1 -1
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/profile.d.ts +1 -1
- package/mcp-server/dist/tools/profile.d.ts.map +1 -1
- package/mcp-server/dist/tools/profile.js +1 -1
- package/mcp-server/dist/tools/profile.js.map +1 -1
- package/mcp-server/dist/tools/recall.d.ts +1 -1
- package/mcp-server/dist/tools/recall.d.ts.map +1 -1
- package/mcp-server/dist/tools/recall.js +1 -1
- package/mcp-server/dist/tools/recall.js.map +1 -1
- package/mcp-server/dist/tools/reflect.d.ts +1 -1
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
- package/mcp-server/dist/tools/reflect.js +1 -1
- package/mcp-server/dist/tools/reflect.js.map +1 -1
- package/mcp-server/dist/tools/remember.d.ts +1 -1
- package/mcp-server/dist/tools/remember.d.ts.map +1 -1
- package/mcp-server/dist/tools/remember.js +13 -11
- package/mcp-server/dist/tools/remember.js.map +1 -1
- package/mcp-server/dist/tools/restore.d.ts +1 -1
- package/mcp-server/dist/tools/restore.d.ts.map +1 -1
- package/mcp-server/dist/tools/restore.js +1 -1
- package/mcp-server/dist/tools/restore.js.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
- package/mcp-server/dist/tools/sync.d.ts +1 -1
- package/mcp-server/dist/tools/sync.d.ts.map +1 -1
- package/mcp-server/dist/tools/sync.js +1 -1
- package/mcp-server/dist/tools/sync.js.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
- package/mcp-server/dist/tools/utils.d.ts +1 -1
- package/mcp-server/dist/tools/utils.d.ts.map +1 -1
- package/mcp-server/dist/tools/utils.js +1 -1
- package/mcp-server/dist/tools/utils.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/download.js +1 -1
- package/mcp-server/dist/tools/youtube/download.js.map +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/media.js +1 -1
- package/mcp-server/dist/tools/youtube/media.js.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/source.js +1 -1
- package/mcp-server/dist/tools/youtube/source.js.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.js +4 -9
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
- package/mcp-server/dist/tui.js +18 -5
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +3 -3
- package/mcp-server/package.json +3 -3
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/browser-bench.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/categories.ts +77 -32
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/desktop-bench.ts +1 -1
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +34 -15
- package/mcp-server/src/tools/bluesky.ts +1 -1
- package/mcp-server/src/tools/browser-advanced.ts +1 -1
- package/mcp-server/src/tools/browser-batch.ts +1 -1
- package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
- package/mcp-server/src/tools/browser-interact.ts +1 -1
- package/mcp-server/src/tools/browser-navigate.ts +1 -1
- package/mcp-server/src/tools/browser-observe.ts +1 -1
- package/mcp-server/src/tools/card-tools.ts +1 -1
- package/mcp-server/src/tools/category.ts +6 -1
- package/mcp-server/src/tools/choice.ts +1 -1
- package/mcp-server/src/tools/computer.ts +1 -1
- package/mcp-server/src/tools/discord-channel.ts +1 -1
- package/mcp-server/src/tools/discord-guild.ts +1 -1
- package/mcp-server/src/tools/discord-member.ts +1 -1
- package/mcp-server/src/tools/discord-message.ts +1 -1
- package/mcp-server/src/tools/discord-onboarding.ts +1 -1
- package/mcp-server/src/tools/discord-role.ts +1 -1
- package/mcp-server/src/tools/discord-thread.ts +1 -1
- package/mcp-server/src/tools/discord-webhook.ts +1 -1
- package/mcp-server/src/tools/fb-groups.ts +1 -1
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git-tools.ts +1 -1
- package/mcp-server/src/tools/gsc/experience.ts +1 -1
- package/mcp-server/src/tools/gsc/indexing.ts +1 -1
- package/mcp-server/src/tools/gsc/inspect.ts +1 -1
- package/mcp-server/src/tools/gsc/navigate.ts +1 -1
- package/mcp-server/src/tools/gsc/performance.ts +1 -1
- package/mcp-server/src/tools/gsc/settings.ts +1 -1
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
- package/mcp-server/src/tools/loop.ts +1 -1
- package/mcp-server/src/tools/memories.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +1 -1
- package/mcp-server/src/tools/profile.ts +1 -1
- package/mcp-server/src/tools/recall.ts +1 -1
- package/mcp-server/src/tools/reflect.ts +1 -1
- package/mcp-server/src/tools/remember.ts +13 -12
- package/mcp-server/src/tools/restore.ts +1 -1
- package/mcp-server/src/tools/style-guide-tools.ts +1 -1
- package/mcp-server/src/tools/sync.ts +1 -1
- package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
- package/mcp-server/src/tools/utils.ts +1 -1
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
- package/mcp-server/src/tools/youtube/analytics.ts +1 -1
- package/mcp-server/src/tools/youtube/download.ts +1 -1
- package/mcp-server/src/tools/youtube/media.ts +1 -1
- package/mcp-server/src/tools/youtube/metadata.ts +1 -1
- package/mcp-server/src/tools/youtube/navigate.ts +1 -1
- package/mcp-server/src/tools/youtube/playlist.ts +1 -1
- package/mcp-server/src/tools/youtube/schedule.ts +1 -1
- package/mcp-server/src/tools/youtube/source.ts +1 -1
- package/mcp-server/src/tools/youtube/upload.ts +4 -12
- package/mcp-server/src/tui.ts +24 -10
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +645 -57
- package/neural-interface/i18n/pt-BR.json +644 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
- package/neural-interface/lib/agent-sandbox-setting.js +57 -0
- package/neural-interface/lib/assistant-api.js +274 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +312 -48
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +180 -0
- package/neural-interface/lib/codex-accounts.js +502 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +122 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/exec-loop-command.js +3 -1
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +163 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +22 -4
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +96 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-indexer.js +3 -1
- package/neural-interface/lib/session-title-generator.js +6 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +542 -0
- package/neural-interface/public/shared/account-usage.js +79 -0
- package/neural-interface/public/shared/account-view.js +336 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +39 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +219 -80
- package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +160 -29
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +49 -108
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
- package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +490 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +281 -32
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +262 -217
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-explorer.js +2 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +8 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +598 -7
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/scripts/rebuild-pty.js +14 -2
- package/neural-interface/server.js +1384 -679
- package/npm-shrinkwrap.json +4424 -0
- package/package.json +57 -2
- package/postinstall.js +61 -47
- package/preuninstall.js +8 -35
- package/setup.js +80 -47
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/schedule.md +1 -1
- package/updater.cmd +2 -2
- package/updater.mjs +97 -3
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
// unavailable | not_installed | installing | install_failed | ready |
|
|
13
13
|
// linking (method qr|code; phase starting|waiting|scanned) | link_expired |
|
|
14
14
|
// confirm_owner (kind self|code) | connected | reconnecting | paused |
|
|
15
|
-
// logged_out (reason removed|inactive|banned) | error (code)
|
|
15
|
+
// logged_out (reason removed|inactive|banned|invalid_session) | error (code)
|
|
16
|
+
// `warning` ('replaced' | 'forbidden' | null) rides along: a saved session
|
|
17
|
+
// that another copy took over (440) or that WhatsApp refuses (403) keeps
|
|
18
|
+
// retrying every 5 minutes, and Settings says why until it is stable again.
|
|
16
19
|
//
|
|
17
20
|
// Secrets keep to one path each: a link QR or pairing code only goes down the
|
|
18
21
|
// NDJSON response of the tab that asked for it, the claim code and wa.me link
|
|
@@ -30,14 +33,16 @@ import { createWhatsAppApi } from './api.js';
|
|
|
30
33
|
import { brainChoiceView, checkBrainChoice, cleanBrainChoice } from './brain.js';
|
|
31
34
|
import { createWhatsAppBridge, outboxPlan } from './bridge.js';
|
|
32
35
|
import { parseCommand } from './commands.js';
|
|
33
|
-
import { createWhatsAppConfigStore, currentLevel, MODES, SERVICE_DEFAULTS, USER_DEFAULTS, USER_FIELDS } from './config.js';
|
|
36
|
+
import { createWhatsAppConfigStore, currentLevel, MODES, SERVICE_DEFAULTS, TRANSCRIPTION_FIELDS, USER_DEFAULTS, USER_FIELDS } from './config.js';
|
|
34
37
|
import { chunk, toWhatsApp } from './format.js';
|
|
35
38
|
import { composePrompt, createCoalescer } from './inbound.js';
|
|
36
39
|
import { createWhatsAppInstaller, NPM_CI_ARGS } from './installer.js';
|
|
37
40
|
import { createWhatsAppManager } from './manager.js';
|
|
38
41
|
import { isInside, resolveWhatsAppPaths } from './paths.js';
|
|
39
42
|
import { renderQrSvg } from './qr-svg.js';
|
|
40
|
-
import {
|
|
43
|
+
import { CONN_WARNINGS, FILE_SEND_AS, LIMITS, VOICE_MARKER } from './protocol.js';
|
|
44
|
+
import { resolveTranscriptionTools, TRANSCRIPTION_LIMITS } from './transcription.js';
|
|
45
|
+
import { maskNumber, redactFilePath, redactLogLine, redactRequestedPath, redactWa } from './redact.js';
|
|
41
46
|
import { providerLabel, remoteBrainLimited, remoteBrainNotice } from '../remote-policy.js';
|
|
42
47
|
import { normalizePhone, PHONE_ERRORS } from '../../public/shared/whatsapp/wa-phone.js';
|
|
43
48
|
|
|
@@ -51,12 +56,12 @@ export const SERVICE_LIMITS = Object.freeze({
|
|
|
51
56
|
escalationMs: 5 * MINUTE, // the phone has this long to answer ALLOW <code>
|
|
52
57
|
escalationTries: 3,
|
|
53
58
|
escalationEchoMs: 10 * MINUTE, // a late ALLOW after this is passed on as text
|
|
54
|
-
autonomousMs: 8 * HOUR,
|
|
55
59
|
testEveryMs: 20_000,
|
|
56
60
|
activityMax: 200,
|
|
57
61
|
logRetentionDays: 7,
|
|
58
62
|
broadcastDebounceMs: 150,
|
|
59
63
|
inactiveAfterMs: 13 * DAY, // logged out after this long without a connection reads as "inactive"
|
|
64
|
+
warningStableMs: 10 * MINUTE, // a 440 / 403 warning clears after this long connected (the host's stable time)
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
export const TEST_MESSAGE = 'SynaBun test message. If you can read this, WhatsApp is working.';
|
|
@@ -80,7 +85,6 @@ export function whatsappError(status, code, message, field) {
|
|
|
80
85
|
return error;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
const hhmm = (ms) => new Date(ms).toTimeString().slice(0, 5);
|
|
84
88
|
const ymd = (ms) => { const d = new Date(ms); return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; };
|
|
85
89
|
const pick = (obj, keys) => Object.fromEntries(keys.map((key) => [key, obj[key]]));
|
|
86
90
|
/** "ABCD1234" → "ABCD-1234" (how WhatsApp shows a pairing code). */
|
|
@@ -104,6 +108,8 @@ export function formatPairingCode(code) {
|
|
|
104
108
|
* @param {Function} [deps.bridgeFactory] (deps) → createWhatsAppBridge-shaped object
|
|
105
109
|
* @param {() => object|null} [deps.getDefaultBrain] the Assistant panel's brain ("Same as the Assistant")
|
|
106
110
|
* @param {() => object|null} [deps.getCatalog] the Assistant's model catalog ({ peek(), full() }): the WhatsApp brain is checked against it
|
|
111
|
+
* @param {Function} [deps.checkTranscription] ({ home, enginePath, modelPath, ffmpegPath, env, platform }) → what voice notes
|
|
112
|
+
* need and lack (transcription.js resolveTranscriptionTools; tests pass a fake). Read only: nothing is run here.
|
|
107
113
|
*/
|
|
108
114
|
export function createWhatsAppService({
|
|
109
115
|
dataHome,
|
|
@@ -128,6 +134,7 @@ export function createWhatsAppService({
|
|
|
128
134
|
policy = undefined,
|
|
129
135
|
getDefaultBrain = null,
|
|
130
136
|
getCatalog = null,
|
|
137
|
+
checkTranscription = resolveTranscriptionTools,
|
|
131
138
|
rng = undefined,
|
|
132
139
|
bridgeLimits = undefined,
|
|
133
140
|
now = Date.now,
|
|
@@ -140,11 +147,22 @@ export function createWhatsAppService({
|
|
|
140
147
|
const disabled = String(env.SYNABUN_WHATSAPP || '').trim().toLowerCase() === 'off';
|
|
141
148
|
const fake = env.SYNABUN_WHATSAPP_FAKE === '1';
|
|
142
149
|
const paths = pathsOverride || resolveWhatsAppPaths({ dataHome, env, platform, extraBackupRoots });
|
|
143
|
-
|
|
150
|
+
// Every line passes redactWa, but send_file's audit line, whose exact shape redact.js fileAuditLine checks.
|
|
151
|
+
const say = (message, level = 'info') => { try { log(`[whatsapp] ${redactLogLine(message)}`, level); } catch {} };
|
|
144
152
|
// A refused SYNABUN_WHATSAPP_HOME (inside a backed-up folder) says so once, in the server log.
|
|
145
153
|
for (const warning of paths.warnings || []) say(warning, 'warn');
|
|
146
154
|
|
|
147
|
-
const config = createWhatsAppConfigStore({
|
|
155
|
+
const config = createWhatsAppConfigStore({
|
|
156
|
+
get: getKvConfig, set: setKvConfig, now, log: (tag, msg) => say(`${tag}: ${msg}`, 'warn'),
|
|
157
|
+
// An 8-hour Autonomous of an earlier version was read (and stored) as Ask: one Activity line says so.
|
|
158
|
+
// Called from a config read, so never before `activity` below exists: no read happens while this function builds.
|
|
159
|
+
onLegacyAutonomous: () => {
|
|
160
|
+
try {
|
|
161
|
+
record('security', 'Autonomous from an earlier version ended; turn it on again to keep it on without a time limit');
|
|
162
|
+
notify();
|
|
163
|
+
} catch {}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
148
166
|
const installer = disabled ? null : installerFactory({ dataHome, paths, env, platform, log: (msg, level) => say(msg, level), now });
|
|
149
167
|
const manager = disabled ? null : managerFactory({ dataHome, paths, env, platform, log: (msg, level) => say(msg, level), now });
|
|
150
168
|
|
|
@@ -160,15 +178,18 @@ export function createWhatsAppService({
|
|
|
160
178
|
fatal: null, // { code, message, at }
|
|
161
179
|
escalation: null, // { code, expiresAt, attemptsLeft, timer }
|
|
162
180
|
escalationEndedAt: 0,
|
|
163
|
-
autonomousTimer: null,
|
|
164
181
|
retentionTimer: null,
|
|
165
182
|
lastConn: null, // last conn snapshot (for transitions)
|
|
166
183
|
lastStateAt: 0,
|
|
184
|
+
lastDisconnectAt: null, // conn.lastDisconnect.at of the last disconnect already looked at
|
|
185
|
+
warning: null, // { kind: 'replaced'|'forbidden', since } while a 440 / 403 episode lasts
|
|
186
|
+
warningTimer: null, // clears the warning once the connection was open for warningStableMs
|
|
167
187
|
connectedAt: 0,
|
|
168
188
|
reconnectAttempt: 0,
|
|
169
189
|
wasConnected: false,
|
|
170
190
|
testAt: 0,
|
|
171
191
|
notesSkipped: 0, // self-chat notes without the "sb" trigger (never logged)
|
|
192
|
+
transcription: null, // { at, key, value }: what voice notes need and lack, looked at again at most every minute
|
|
172
193
|
bridge: null,
|
|
173
194
|
rev: 0,
|
|
174
195
|
lastSignature: '',
|
|
@@ -188,7 +209,7 @@ export function createWhatsAppService({
|
|
|
188
209
|
return value;
|
|
189
210
|
}
|
|
190
211
|
function managerStatus() {
|
|
191
|
-
const empty = { host: { state: 'stopped', pid: null, restarts: 0, lastError: null }, conn: { state: 'idle', mode: null, registered: false, me: null, owner: null, lastDisconnect: null, retryInMs: null, counters: {}, paused: false, awaitingConfirmUntil: null, claim: null, lastError: null }, runtime: { loaded: false, version: null, fake: false, error: null } };
|
|
212
|
+
const empty = { host: { state: 'stopped', pid: null, restarts: 0, lastError: null, retryInMs: null }, conn: { state: 'idle', mode: null, registered: false, me: null, owner: null, lastDisconnect: null, retryInMs: null, counters: {}, paused: false, awaitingConfirmUntil: null, claim: null, lastError: null }, runtime: { loaded: false, version: null, fake: false, error: null } };
|
|
192
213
|
if (!manager) return empty;
|
|
193
214
|
try { const s = manager.status(); return { host: { ...empty.host, ...(s?.host || {}) }, conn: { ...empty.conn, ...(s?.conn || {}) }, runtime: { ...empty.runtime, ...(s?.runtime || {}) } }; }
|
|
194
215
|
catch { return empty; }
|
|
@@ -203,7 +224,10 @@ export function createWhatsAppService({
|
|
|
203
224
|
}
|
|
204
225
|
// The host's own "sb" filter stays off (selfTrigger 'all'): it would drop /stop,
|
|
205
226
|
// /status and card answers too. selfTrigger 'prefix' is applied here (applyTrigger).
|
|
206
|
-
|
|
227
|
+
// The voice-note settings go to the host, which transcribes (lib/whatsapp/transcription.js).
|
|
228
|
+
function prefsOf(cfg) {
|
|
229
|
+
return { mode: cfg.mode, selfTrigger: 'all', ...pick(cfg, TRANSCRIPTION_FIELDS) };
|
|
230
|
+
}
|
|
207
231
|
function configureHost(cfg = config.read()) {
|
|
208
232
|
if (!manager) return;
|
|
209
233
|
Promise.resolve(manager.configure?.(prefsOf(cfg))).catch(() => {});
|
|
@@ -212,6 +236,32 @@ export function createWhatsAppService({
|
|
|
212
236
|
if (cfg.replyLabel) return cfg.replyLabel;
|
|
213
237
|
return cfg.mode === 'dedicated' ? '' : 'SynaBun:';
|
|
214
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Voice notes as Settings shows them: { enabled, available, missing, invalid, language, home }. The
|
|
241
|
+
* same resolution the host makes before it downloads a note (the host checks again right before a
|
|
242
|
+
* program runs); missing / invalid are 'engine' | 'model' | 'ffmpeg'. Read only, cached a minute.
|
|
243
|
+
*/
|
|
244
|
+
function transcriptionView(cfg = config.read()) {
|
|
245
|
+
const key = JSON.stringify([cfg.transcriptionEnginePath, cfg.transcriptionModelPath, cfg.transcriptionFfmpegPath]);
|
|
246
|
+
let tools = state.transcription && state.transcription.key === key && now() - state.transcription.at < TRANSCRIPTION_LIMITS.recheckMs ? state.transcription.value : null;
|
|
247
|
+
if (!tools) {
|
|
248
|
+
try {
|
|
249
|
+
tools = checkTranscription({ home: paths.transcriptionDir, enginePath: cfg.transcriptionEnginePath, modelPath: cfg.transcriptionModelPath, ffmpegPath: cfg.transcriptionFfmpegPath, env, platform });
|
|
250
|
+
} catch {
|
|
251
|
+
tools = null;
|
|
252
|
+
}
|
|
253
|
+
if (!tools || !Array.isArray(tools.missing)) tools = { missing: ['engine', 'model', 'ffmpeg'], invalid: [], available: false };
|
|
254
|
+
state.transcription = { at: now(), key, value: tools };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
enabled: cfg.transcriptionEnabled === true,
|
|
258
|
+
available: tools.available === true,
|
|
259
|
+
missing: [...tools.missing],
|
|
260
|
+
invalid: Array.isArray(tools.invalid) ? [...tools.invalid] : [],
|
|
261
|
+
language: cfg.transcriptionLanguage,
|
|
262
|
+
home: paths.transcriptionDir || null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
215
265
|
|
|
216
266
|
// ── the tab's state machine ────────────────────────────────────────────────
|
|
217
267
|
function logoutReason(cfg) {
|
|
@@ -221,7 +271,6 @@ export function createWhatsAppService({
|
|
|
221
271
|
function errorCodeOf(conn) {
|
|
222
272
|
if (state.fatal?.code && state.fatal.code !== 'LOGGED_OUT') return state.fatal.code;
|
|
223
273
|
const reason = String(conn.lastError || '');
|
|
224
|
-
if (reason === 'reconnect_limit') return 'RECONNECT_LIMIT';
|
|
225
274
|
if (reason === 'pairing_code_failed') return 'PAIRING_FAILED';
|
|
226
275
|
return 'SESSION_ERROR';
|
|
227
276
|
}
|
|
@@ -270,7 +319,7 @@ export function createWhatsAppService({
|
|
|
270
319
|
const tab = tabState();
|
|
271
320
|
const inst = installerStatus();
|
|
272
321
|
const conn = managerStatus().conn;
|
|
273
|
-
const level = currentLevel(cfg
|
|
322
|
+
const level = currentLevel(cfg);
|
|
274
323
|
const counters = {};
|
|
275
324
|
for (const [key, value] of Object.entries(conn.counters || {})) if (Number.isFinite(value)) counters[key] = value;
|
|
276
325
|
return {
|
|
@@ -280,13 +329,18 @@ export function createWhatsAppService({
|
|
|
280
329
|
phase: tab.phase || null,
|
|
281
330
|
mode: cfg.mode,
|
|
282
331
|
level,
|
|
283
|
-
|
|
332
|
+
// Autonomous has no time limit: kept for older readers, always null.
|
|
333
|
+
levelExpiresAt: null,
|
|
284
334
|
paused: cfg.paused,
|
|
285
335
|
pausedBy: cfg.pausedBy,
|
|
286
336
|
// The owner's switch for computer use from WhatsApp (what it allows per level: remoteComputerUse).
|
|
287
337
|
computerUse: cfg.computerUse === true,
|
|
288
338
|
// Which brain runs the WhatsApp conversation: null = same as the Assistant.
|
|
289
339
|
brain: cfg.brain ? { provider: cfg.brain.provider, model: cfg.brain.model, effort: cfg.brain.effort || null } : null,
|
|
340
|
+
// A 440 / 403 the link keeps retrying: 'replaced' | 'forbidden' | null (its own field, so a clear re-reads every tab).
|
|
341
|
+
warning: state.warning?.kind || null,
|
|
342
|
+
// Voice notes on and set up (no path, no list of what is missing: the status has those).
|
|
343
|
+
transcription: (() => { const v = transcriptionView(cfg); return { enabled: v.enabled, available: v.available }; })(),
|
|
290
344
|
rev: state.rev,
|
|
291
345
|
connector: { installed: !!inst.installed, stage: state.install?.stage || null, updateAvailable: !!inst.outdated, errorCode: state.installError?.code || null },
|
|
292
346
|
counters,
|
|
@@ -320,9 +374,17 @@ export function createWhatsAppService({
|
|
|
320
374
|
if (state.logFile !== file) { state.logFile = file; try { chmodSync(file, 0o600); } catch {} }
|
|
321
375
|
} catch {}
|
|
322
376
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
377
|
+
/**
|
|
378
|
+
* One activity row. `text` (a message body) is kept only while activityText is on. `filePath`: a
|
|
379
|
+
* file's path, appended through redactFilePath (redactWa would read most paths as key material);
|
|
380
|
+
* the rest of `detail` is SynaBun's own words and goes through redactWa as always.
|
|
381
|
+
*/
|
|
382
|
+
function record(kind, detail, text = null, { filePath = null, requested = false } = {}) {
|
|
383
|
+
const words = redactWa(String(detail || ''));
|
|
384
|
+
// A path can hold control characters (a file or a link target named with a newline): never a second log line.
|
|
385
|
+
// A refused request's path is the model's own text: key-shaped runs in it are masked too (redactRequestedPath).
|
|
386
|
+
const shownPath = filePath ? (requested ? redactRequestedPath : redactFilePath)(filePath).replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, '?') : '';
|
|
387
|
+
const entry = { at: now(), kind, detail: (filePath ? `${words} ${shownPath}` : words).slice(0, filePath ? 600 : 300) };
|
|
326
388
|
if (text && config.read().activityText) entry.text = redactWa(String(text)).slice(0, 2000);
|
|
327
389
|
activity.push(entry);
|
|
328
390
|
if (activity.length > L.activityMax) activity.splice(0, activity.length - L.activityMax);
|
|
@@ -338,12 +400,19 @@ export function createWhatsAppService({
|
|
|
338
400
|
if (name.slice(9, 17) < cutoff) { try { unlinkSync(path.join(paths.logsDir, name)); } catch {} }
|
|
339
401
|
}
|
|
340
402
|
}
|
|
403
|
+
// Why a voice note was not transcribed (the host's reason), for the activity log.
|
|
404
|
+
const AUDIO_REASONS = Object.freeze({
|
|
405
|
+
transcription_disabled: 'transcription is off', transcription_unavailable: 'transcription is not set up',
|
|
406
|
+
too_large: 'over 20 MB', too_long: 'over 10 minutes', stale: 'sent while SynaBun was offline', transcription_failed: 'transcription failed',
|
|
407
|
+
});
|
|
341
408
|
function describeInbound(message) {
|
|
342
409
|
const parts = [];
|
|
343
|
-
if (
|
|
410
|
+
if (message.voice === true) parts.push('a voice note (transcribed on this computer)');
|
|
411
|
+
else if (String(message.text || '').trim()) parts.push('text');
|
|
344
412
|
const images = Array.isArray(message.images) ? message.images.length : 0;
|
|
345
413
|
if (images) parts.push(`${images} image${images === 1 ? '' : 's'}`);
|
|
346
|
-
if (message.unsupported
|
|
414
|
+
if (message.unsupported?.type === 'audio' && Object.hasOwn(AUDIO_REASONS, String(message.unsupported.reason))) parts.push(`a voice note (not transcribed: ${AUDIO_REASONS[message.unsupported.reason]})`);
|
|
415
|
+
else if (message.unsupported) parts.push(`${String(message.unsupported.type || 'unsupported')} (not supported)`);
|
|
347
416
|
const flags = [message.forwarded ? 'forwarded' : null, message.quoted ? (message.quoted.fromBot ? 'reply to SynaBun' : 'quote') : null].filter(Boolean);
|
|
348
417
|
return `Message from you: ${parts.join(', ') || 'empty'}${flags.length ? ` (${flags.join(', ')})` : ''}`;
|
|
349
418
|
}
|
|
@@ -352,11 +421,12 @@ export function createWhatsAppService({
|
|
|
352
421
|
// Why a refused reply was dropped (the bridge's outboxPlan decides; the phone cannot be told).
|
|
353
422
|
const DROPPED_BECAUSE = {
|
|
354
423
|
PAUSED: 'sending is paused because the owner looked different than expected. See Setup & link',
|
|
355
|
-
HELD: 'the WhatsApp connector
|
|
424
|
+
HELD: 'the WhatsApp connector does not match this SynaBun version (an update was installed while it ran). Restart SynaBun',
|
|
356
425
|
};
|
|
426
|
+
const NO_MANAGER = Object.freeze({ ok: false, code: 'UNAVAILABLE', message: 'WhatsApp is not running on this computer' });
|
|
357
427
|
const transport = {
|
|
358
428
|
send: async (text, opts = {}) => {
|
|
359
|
-
const result = await manager.send(text, opts);
|
|
429
|
+
const result = manager ? await manager.send(text, opts) : NO_MANAGER;
|
|
360
430
|
if (result?.ok) record('reply', `Sent a message (${String(text).length} characters)`, text);
|
|
361
431
|
else {
|
|
362
432
|
const code = String(result?.code || 'unknown');
|
|
@@ -368,9 +438,9 @@ export function createWhatsAppService({
|
|
|
368
438
|
notify();
|
|
369
439
|
return result;
|
|
370
440
|
},
|
|
371
|
-
react: (id, name) => manager.react(id, name),
|
|
372
|
-
presence: (value) => manager.presence(value),
|
|
373
|
-
markRead: (ids) => manager.markRead(ids),
|
|
441
|
+
react: async (id, name) => (manager ? manager.react(id, name) : NO_MANAGER),
|
|
442
|
+
presence: async (value) => (manager ? manager.presence(value) : NO_MANAGER),
|
|
443
|
+
markRead: async (ids) => (manager ? manager.markRead(ids) : NO_MANAGER),
|
|
374
444
|
connected: () => connectedNow(),
|
|
375
445
|
};
|
|
376
446
|
const bridgeConfig = { read: () => config.read(), write: (patch) => config.write(patch) };
|
|
@@ -409,7 +479,7 @@ export function createWhatsAppService({
|
|
|
409
479
|
}
|
|
410
480
|
function bridgeStatus() { try { return state.bridge?.status?.() || null; } catch { return null; } }
|
|
411
481
|
function refreshBridge() { try { state.bridge?.refresh?.(); } catch {} }
|
|
412
|
-
/** A notice from SynaBun itself (
|
|
482
|
+
/** A notice from SynaBun itself (the Autonomous request and its ALLOW answer): straight to the phone, labelled like the bridge's replies. */
|
|
413
483
|
async function notice(text) {
|
|
414
484
|
if (!manager || !connectedNow()) return false;
|
|
415
485
|
const prefix = replyPrefix();
|
|
@@ -417,7 +487,7 @@ export function createWhatsAppService({
|
|
|
417
487
|
return !!result?.ok;
|
|
418
488
|
}
|
|
419
489
|
|
|
420
|
-
// ── autonomous: the phone's ALLOW code
|
|
490
|
+
// ── autonomous: the phone's ALLOW code; on until the owner lowers it ───────
|
|
421
491
|
function endEscalation(reason) {
|
|
422
492
|
const esc = state.escalation;
|
|
423
493
|
if (!esc) return;
|
|
@@ -439,36 +509,15 @@ export function createWhatsAppService({
|
|
|
439
509
|
esc.timer?.unref?.();
|
|
440
510
|
state.escalation = esc;
|
|
441
511
|
record('security', 'Autonomous requested on this computer; waiting for the ALLOW code from the phone');
|
|
442
|
-
notice('Autonomous was requested on your computer. To turn it on
|
|
512
|
+
notice('Autonomous was requested on your computer. To turn it on, reply ALLOW followed by the 4-digit code shown in SynaBun, within 5 minutes. Ignore this message if it was not you.').catch(() => {});
|
|
443
513
|
notify();
|
|
444
514
|
return { code, expiresAt: esc.expiresAt, attemptsLeft: esc.attemptsLeft };
|
|
445
515
|
}
|
|
446
|
-
function armAutonomousTimer() {
|
|
447
|
-
T.clearTimeout(state.autonomousTimer);
|
|
448
|
-
state.autonomousTimer = null;
|
|
449
|
-
const cfg = config.read();
|
|
450
|
-
if (cfg.level !== 'autonomous') return;
|
|
451
|
-
const left = (cfg.autonomousUntil || 0) - now();
|
|
452
|
-
if (left <= 0) { expireAutonomous(); return; }
|
|
453
|
-
// setTimeout caps at 2^31-1 ms; 8 h is far below it.
|
|
454
|
-
state.autonomousTimer = T.setTimeout(expireAutonomous, left);
|
|
455
|
-
state.autonomousTimer?.unref?.();
|
|
456
|
-
}
|
|
457
|
-
function expireAutonomous() {
|
|
458
|
-
state.autonomousTimer = null;
|
|
459
|
-
const cfg = config.read();
|
|
460
|
-
if (cfg.level !== 'autonomous') return;
|
|
461
|
-
config.write({ level: 'ask', autonomousUntil: null });
|
|
462
|
-
refreshBridge();
|
|
463
|
-
record('security', 'Autonomous ended; back to Ask on my phone');
|
|
464
|
-
notice('Autonomous ended: I ask on your phone again before acting.').catch(() => {});
|
|
465
|
-
notify();
|
|
466
|
-
}
|
|
467
516
|
const sameCode = (a, b) => { const x = Buffer.from(String(a)); const y = Buffer.from(String(b)); return x.length === y.length && timingSafeEqual(x, y); };
|
|
468
|
-
/** "ALLOW 1234"
|
|
517
|
+
/** "ALLOW 1234" typed by the owner: complete, refuse or explain. → true when consumed. A voice note never is one. */
|
|
469
518
|
function handleAllow(message) {
|
|
470
519
|
const match = /^\s*allow\s+(\d{4})\s*$/i.exec(String(message.text || ''));
|
|
471
|
-
if (!match || message.forwarded || (Array.isArray(message.images) && message.images.length)) return false;
|
|
520
|
+
if (!match || message.forwarded || message.voice === true || (Array.isArray(message.images) && message.images.length)) return false;
|
|
472
521
|
const esc = state.escalation;
|
|
473
522
|
if (!esc) {
|
|
474
523
|
if (state.escalationEndedAt && now() - state.escalationEndedAt < L.escalationEchoMs) {
|
|
@@ -493,17 +542,55 @@ export function createWhatsAppService({
|
|
|
493
542
|
}
|
|
494
543
|
return true;
|
|
495
544
|
}
|
|
496
|
-
|
|
497
|
-
config.write({ level: 'autonomous', autonomousUntil:
|
|
545
|
+
// No time limit: Autonomous stays on, restarts included, until the owner lowers it in Settings.
|
|
546
|
+
config.write({ level: 'autonomous', autonomousUntil: null });
|
|
498
547
|
endEscalation('allowed');
|
|
499
548
|
refreshBridge();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
notice(`Autonomous is on until ${hhmm(until)}. I act without asking for 8 hours, then go back to Ask. Send /pause to stop everything.`).catch(() => {});
|
|
549
|
+
record('security', 'Autonomous turned on from the phone; it stays on until it is turned off in Settings → WhatsApp');
|
|
550
|
+
notice('Autonomous is on. It stays on until you turn it off in Settings → WhatsApp on your computer. /pause pauses your messages and /stop stops the current task; neither changes the level.').catch(() => {});
|
|
503
551
|
notify();
|
|
504
552
|
return true;
|
|
505
553
|
}
|
|
506
554
|
|
|
555
|
+
// ── a saved session another copy took over (440) or WhatsApp refuses (403) ─
|
|
556
|
+
// The host keeps retrying it every 5 minutes; this is what Settings shows meanwhile. One
|
|
557
|
+
// Activity line opens an episode and one closes it, after 10 minutes connected: the
|
|
558
|
+
// connects and drops in between are not logged one by one (the server log has each).
|
|
559
|
+
const WARNING_ACTIVITY = Object.freeze({
|
|
560
|
+
replaced: 'Another copy is using this WhatsApp session; SynaBun keeps retrying every 5 minutes',
|
|
561
|
+
forbidden: 'WhatsApp refused this account (it may be restricted or banned); SynaBun keeps retrying every 5 minutes',
|
|
562
|
+
});
|
|
563
|
+
function noteWarning(kind) {
|
|
564
|
+
if (!WARNING_ACTIVITY[kind]) return;
|
|
565
|
+
clearWarningTimer();
|
|
566
|
+
if (state.warning?.kind === kind) return;
|
|
567
|
+
state.warning = { kind, since: now() };
|
|
568
|
+
record('security', WARNING_ACTIVITY[kind]);
|
|
569
|
+
}
|
|
570
|
+
function clearWarningTimer() {
|
|
571
|
+
if (!state.warningTimer) return;
|
|
572
|
+
T.clearTimeout(state.warningTimer);
|
|
573
|
+
state.warningTimer = null;
|
|
574
|
+
}
|
|
575
|
+
function clearWarning() {
|
|
576
|
+
clearWarningTimer();
|
|
577
|
+
state.warning = null;
|
|
578
|
+
}
|
|
579
|
+
/** The connection is open: once it stays so for warningStableMs, the episode is over. */
|
|
580
|
+
function armWarningClear() {
|
|
581
|
+
if (!state.warning || state.warningTimer) return;
|
|
582
|
+
state.warningTimer = T.setTimeout(() => {
|
|
583
|
+
state.warningTimer = null;
|
|
584
|
+
if (!state.warning || !connectedNow()) return;
|
|
585
|
+
state.warning = null;
|
|
586
|
+
record('state', 'The WhatsApp connection is stable again');
|
|
587
|
+
notify();
|
|
588
|
+
}, L.warningStableMs);
|
|
589
|
+
state.warningTimer?.unref?.();
|
|
590
|
+
}
|
|
591
|
+
// A saved session WhatsApp itself rejected: a stream error naming a bad session, or a QR for linked credentials.
|
|
592
|
+
const INVALID_SESSION_ERRORS = new Set(['badSession', 'session_rejected']);
|
|
593
|
+
|
|
507
594
|
// ── manager events ─────────────────────────────────────────────────────────
|
|
508
595
|
function onState(snapshot) {
|
|
509
596
|
const conn = snapshot?.conn || {};
|
|
@@ -511,17 +598,25 @@ export function createWhatsAppService({
|
|
|
511
598
|
state.lastConn = conn;
|
|
512
599
|
state.lastStateAt = now();
|
|
513
600
|
const cfg = config.read();
|
|
601
|
+
// A disconnect this service has not looked at yet (a respawned host's conn repeats the last one).
|
|
602
|
+
const drop = conn.lastDisconnect && conn.lastDisconnect.at !== state.lastDisconnectAt ? conn.lastDisconnect : null;
|
|
603
|
+
if (drop) state.lastDisconnectAt = drop.at;
|
|
604
|
+
const warned = drop && conn.state === 'reconnecting' ? CONN_WARNINGS[drop.code] || null : null;
|
|
605
|
+
const episode = !!state.warning || !!warned;
|
|
514
606
|
if (conn.state === 'open' && prev?.state !== 'open') {
|
|
515
607
|
state.connectedAt = now();
|
|
516
608
|
state.reconnectAttempt = 0;
|
|
517
609
|
state.fatal = null;
|
|
518
610
|
config.write({ setup: { ...(cfg.setup || {}), connectedAt: now(), lastEnd: null, ...(conn.registered && !cfg.setup?.linkedAt ? { linkedAt: now() } : {}) } });
|
|
519
|
-
record('state', 'Connected to WhatsApp');
|
|
611
|
+
if (!episode) record('state', 'Connected to WhatsApp');
|
|
520
612
|
}
|
|
613
|
+
if (conn.state === 'open') armWarningClear();
|
|
614
|
+
else clearWarningTimer();
|
|
521
615
|
if (conn.state === 'reconnecting' && prev?.state !== 'reconnecting') {
|
|
522
616
|
state.reconnectAttempt += 1;
|
|
523
|
-
if (prev?.state === 'open') record('state', 'Connection lost; reconnecting');
|
|
617
|
+
if (prev?.state === 'open' && !episode) record('state', drop ? `Connection lost (code ${drop.code ?? 'none'}, ${drop.reason}); reconnecting` : 'Connection lost; reconnecting');
|
|
524
618
|
}
|
|
619
|
+
if (warned) noteWarning(warned);
|
|
525
620
|
// The link's phase follows the host: a QR/code on screen is "waiting", the phone accepting it is "scanned".
|
|
526
621
|
// Terminal states only count once the host took the link request (a fresh host starts out "unlinked").
|
|
527
622
|
const link = state.link;
|
|
@@ -542,10 +637,14 @@ export function createWhatsAppService({
|
|
|
542
637
|
}
|
|
543
638
|
if (!claim?.active && !conn.owner?.bound && prev?.claim?.active) endClaim({ type: 'expired' });
|
|
544
639
|
}
|
|
545
|
-
|
|
640
|
+
// 'forbidden' is a link attempt WhatsApp refused: a saved session retries a 403 instead (the warning above).
|
|
641
|
+
if (['logged_out', 'forbidden'].includes(conn.state) && prev?.state !== conn.state) {
|
|
642
|
+
onLoggedOut(conn.state === 'forbidden' ? 'banned' : INVALID_SESSION_ERRORS.has(conn.lastError) ? 'invalid_session' : logoutReason(cfg));
|
|
643
|
+
}
|
|
546
644
|
if (conn.state === 'unlinked' && prev && prev.state !== 'unlinked' && !state.link) {
|
|
547
|
-
// Confirmation refused or timed out, or the host wiped the link.
|
|
645
|
+
// Confirmation refused or timed out, or the host wiped the link: nothing is left to retry.
|
|
548
646
|
config.write({ owner: null, setup: cfg.setup ? { ...cfg.setup, linkedAt: null } : null });
|
|
647
|
+
clearWarning();
|
|
549
648
|
}
|
|
550
649
|
// Every state change reaches the bridge (it sends the restart note and flushes its outbox on connect).
|
|
551
650
|
const connected = conn.state === 'open';
|
|
@@ -554,14 +653,21 @@ export function createWhatsAppService({
|
|
|
554
653
|
try { const r = bridge?.onConnection?.({ connected, state: conn.state }); if (r?.catch) r.catch(() => {}); } catch {}
|
|
555
654
|
notify();
|
|
556
655
|
}
|
|
656
|
+
const LOGGED_OUT_ACTIVITY = Object.freeze({
|
|
657
|
+
banned: 'WhatsApp refused this account (it may be banned)',
|
|
658
|
+
inactive: 'WhatsApp logged SynaBun out after a long time offline',
|
|
659
|
+
invalid_session: 'WhatsApp rejected the saved session as invalid; link again to pair this device',
|
|
660
|
+
removed: 'WhatsApp logged SynaBun out (removed from the phone)',
|
|
661
|
+
});
|
|
557
662
|
function onLoggedOut(reason) {
|
|
558
663
|
const cfg = config.read();
|
|
559
664
|
config.write({ owner: null, setup: { ...(cfg.setup || {}), linkedAt: null, lastEnd: { state: 'logged_out', reason, at: now() } } });
|
|
560
|
-
record('security', reason
|
|
665
|
+
record('security', LOGGED_OUT_ACTIVITY[reason] || LOGGED_OUT_ACTIVITY.removed);
|
|
666
|
+
clearWarning();
|
|
561
667
|
endEscalation('logged out');
|
|
562
668
|
dropBridge().catch(() => {});
|
|
563
669
|
// The host wiped the credentials; nothing is left for it to do.
|
|
564
|
-
T.setTimeout(() => { if (!isLinked()) manager
|
|
670
|
+
T.setTimeout(() => { if (!isLinked()) Promise.resolve(manager?.stop?.()).catch(() => {}); }, 1000)?.unref?.();
|
|
565
671
|
}
|
|
566
672
|
function onQr({ qr, expiresAt } = {}) {
|
|
567
673
|
const link = state.link;
|
|
@@ -601,23 +707,52 @@ export function createWhatsAppService({
|
|
|
601
707
|
notify();
|
|
602
708
|
}
|
|
603
709
|
const TRIGGER_RE = /^\s*sb(?:\s+|:\s*|$)/i; // "sb hi", "sb:hi", or a bare "sb" caption on a picture
|
|
710
|
+
// Spoken, the prefix comes back as "SB", "SB," "S.B." or "s b": letters s and b, then no other letter.
|
|
711
|
+
const SPOKEN_TRIGGER_RE = /^\s*s\.?\s?b\.?(?=$|[^\p{L}\p{N}])[\s,:;.!?…-]*/iu;
|
|
604
712
|
const ALLOW_RE = /^\s*allow\s+\d{4}\s*$/i;
|
|
713
|
+
/** A transcribed voice note's text → its transcript (one line) and its caption (after the first "\n"). */
|
|
714
|
+
function voiceParts(text) {
|
|
715
|
+
const body = text.startsWith(VOICE_MARKER) ? text.slice(VOICE_MARKER.length) : text;
|
|
716
|
+
const cut = body.indexOf('\n');
|
|
717
|
+
return cut < 0 ? { transcript: body.trim(), caption: '' } : { transcript: body.slice(0, cut).trim(), caption: body.slice(cut + 1) };
|
|
718
|
+
}
|
|
719
|
+
const voiceText = (transcript, caption) => (transcript ? `${VOICE_MARKER} ${transcript}${caption.trim() ? `\n${caption}` : ''}` : caption);
|
|
720
|
+
/**
|
|
721
|
+
* A voice note in "Message yourself" with selfTrigger 'prefix': it is for SynaBun when its spoken
|
|
722
|
+
* transcript starts with the prefix (stripped; the marker stays), when its caption does (stripped),
|
|
723
|
+
* or when it replies to one of SynaBun's messages. Never a command or an ALLOW code. → the message or null.
|
|
724
|
+
*/
|
|
725
|
+
function voiceTrigger(message, text) {
|
|
726
|
+
const { transcript, caption } = voiceParts(text);
|
|
727
|
+
const spoken = SPOKEN_TRIGGER_RE.exec(transcript);
|
|
728
|
+
if (spoken) {
|
|
729
|
+
const rest = transcript.slice(spoken[0].length).trim();
|
|
730
|
+
return rest || caption.trim() ? { ...message, text: voiceText(rest, caption) } : null;
|
|
731
|
+
}
|
|
732
|
+
const typed = TRIGGER_RE.exec(caption);
|
|
733
|
+
if (typed) return { ...message, text: voiceText(transcript, caption.slice(typed[0].length)) };
|
|
734
|
+
if (message.quoted?.fromBot === true) return message;
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
605
737
|
/**
|
|
606
738
|
* "Message yourself" with selfTrigger 'prefix': only messages that start with
|
|
607
739
|
* "sb " or "sb:" are for SynaBun (the prefix is stripped). Commands (/stop,
|
|
608
740
|
* /status …), the ALLOW answer and replies quoting SynaBun's own messages
|
|
609
|
-
* always pass. Anything else is a note to self:
|
|
741
|
+
* always pass. A voice note: voiceTrigger. Anything else is a note to self:
|
|
742
|
+
* dropped here, never logged.
|
|
610
743
|
* → the message to act on, or null.
|
|
611
744
|
*/
|
|
612
745
|
function applyTrigger(message) {
|
|
613
746
|
const cfg = config.read();
|
|
614
747
|
if (cfg.mode !== 'self' || cfg.selfTrigger !== 'prefix') return message;
|
|
615
748
|
const text = typeof message.text === 'string' ? message.text : String(message.text ?? '');
|
|
749
|
+
if (message.voice === true) return voiceTrigger(message, text);
|
|
616
750
|
const hasImages = Array.isArray(message.images) && message.images.length > 0;
|
|
617
751
|
const match = TRIGGER_RE.exec(text);
|
|
618
752
|
if (match) {
|
|
619
753
|
const rest = text.slice(match[0].length);
|
|
620
|
-
|
|
754
|
+
// A bare "sb" caption on a voice note that was not transcribed still gets the note that says why.
|
|
755
|
+
return rest.trim() || hasImages || message.unsupported?.type === 'audio' ? { ...message, text: rest } : null;
|
|
621
756
|
}
|
|
622
757
|
if (!message.forwarded && ALLOW_RE.test(text)) return message;
|
|
623
758
|
const command = parseCommand(message);
|
|
@@ -653,8 +788,9 @@ export function createWhatsAppService({
|
|
|
653
788
|
state.fatal = { code: String(code || 'ERROR'), message: message ? redactWa(message) : null, at: now() };
|
|
654
789
|
const labels = {
|
|
655
790
|
LOGGED_OUT: 'WhatsApp logged SynaBun out', REPLACED: 'Another copy of this link took over the session', FORBIDDEN: 'WhatsApp refused the connection',
|
|
656
|
-
CONFIRM_TIMEOUT: 'The linked account was not confirmed within 10 minutes and was logged out',
|
|
657
|
-
|
|
791
|
+
CONFIRM_TIMEOUT: 'The linked account was not confirmed within 10 minutes and was logged out',
|
|
792
|
+
CRASH: 'The WhatsApp connector crashed; SynaBun restarts it and reconnects by itself',
|
|
793
|
+
HOST_HELD: 'The WhatsApp connector does not match this SynaBun version and was stopped; restart SynaBun', OWNER_ANOMALY: 'Sending paused: the owner looked different than expected', LOCKED: 'Another SynaBun is using the WhatsApp session files',
|
|
658
794
|
AUTH_PERMS: 'The WhatsApp session files could not be made private (fix the folder permissions, then reconnect)',
|
|
659
795
|
};
|
|
660
796
|
record('error', labels[state.fatal.code] || `WhatsApp connector: ${state.fatal.code}`);
|
|
@@ -679,26 +815,53 @@ export function createWhatsAppService({
|
|
|
679
815
|
function requireAvailable() {
|
|
680
816
|
if (disabled) throw whatsappError(409, 'UNAVAILABLE', 'WhatsApp is turned off on this computer (SYNABUN_WHATSAPP=off).');
|
|
681
817
|
}
|
|
682
|
-
/**
|
|
818
|
+
/** The ops that talk to the host: a service built without a manager answers 409 instead of throwing. */
|
|
819
|
+
function requireManager() {
|
|
820
|
+
if (!manager) throw whatsappError(409, 'UNAVAILABLE', 'WhatsApp is not running on this computer.');
|
|
821
|
+
}
|
|
822
|
+
/** What a connect answered: nothing linked any more clears the link on record. */
|
|
823
|
+
function afterConnect(result, cfg) {
|
|
824
|
+
if (result?.ok) { ensureBridge(); return; }
|
|
825
|
+
if (result?.code === 'NOT_LINKED') config.write({ owner: null, setup: cfg.setup ? { ...cfg.setup, linkedAt: null } : null });
|
|
826
|
+
else state.fatal = { code: result?.code || 'CONNECT_FAILED', message: result?.message ? redactWa(result.message) : null, at: now() };
|
|
827
|
+
}
|
|
828
|
+
/** Start the host and the linked session (autostart, resume, reconnect, turning WhatsApp on). */
|
|
683
829
|
async function run() {
|
|
830
|
+
if (!manager) {
|
|
831
|
+
state.fatal = { code: 'HOST_UNAVAILABLE', message: NO_MANAGER.message, at: now() };
|
|
832
|
+
notify();
|
|
833
|
+
return { ...NO_MANAGER };
|
|
834
|
+
}
|
|
684
835
|
const cfg = config.read();
|
|
685
836
|
wireManager();
|
|
686
837
|
const started = await manager.start(prefsOf(cfg));
|
|
687
838
|
if (!started?.ok) {
|
|
688
839
|
state.fatal = { code: started?.code || 'HOST_UNAVAILABLE', message: started?.message ? redactWa(started.message) : null, at: now() };
|
|
840
|
+
// A host that is slow to start, or that crashed and is respawning, comes up later: the manager
|
|
841
|
+
// keeps the intent and connects on its ready. Not awaited: it may wait for that ready.
|
|
842
|
+
if (config.read().enabled && !state.stopping) {
|
|
843
|
+
Promise.resolve(manager.connect({ purpose: 'run', mode: cfg.mode })).then((result) => { if (result?.ok || result?.code === 'NOT_LINKED') { afterConnect(result, config.read()); notify(); } }, () => {});
|
|
844
|
+
}
|
|
689
845
|
notify();
|
|
690
846
|
return started || { ok: false, code: 'HOST_UNAVAILABLE' };
|
|
691
847
|
}
|
|
692
848
|
const result = await manager.connect({ purpose: 'run', mode: cfg.mode });
|
|
693
|
-
|
|
694
|
-
if (result?.code === 'NOT_LINKED') config.write({ owner: null, setup: cfg.setup ? { ...cfg.setup, linkedAt: null } : null });
|
|
695
|
-
else state.fatal = { code: result?.code || 'CONNECT_FAILED', message: result?.message ? redactWa(result.message) : null, at: now() };
|
|
696
|
-
} else {
|
|
697
|
-
ensureBridge();
|
|
698
|
-
}
|
|
849
|
+
afterConnect(result, cfg);
|
|
699
850
|
notify();
|
|
700
851
|
return result;
|
|
701
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Settings turned WhatsApp off (`enabled: false`): the host stops, and with it every pending retry.
|
|
855
|
+
* The link (and its credentials) stays: turning it on again resumes the saved session.
|
|
856
|
+
*/
|
|
857
|
+
async function stopForDisable() {
|
|
858
|
+
refreshBridge(); // control of the Mac ends first (the bridge re-registers its sessions without computer use)
|
|
859
|
+
if (state.link) finishLink('failed', { type: 'error', code: 'DISABLED' });
|
|
860
|
+
endClaim({ type: 'cancelled' });
|
|
861
|
+
endEscalation('WhatsApp turned off');
|
|
862
|
+
clearWarning();
|
|
863
|
+
if (manager) { try { await manager.stop(); } catch (error) { say(`stop: ${error?.message || error}`, 'warn'); } }
|
|
864
|
+
}
|
|
702
865
|
async function start() {
|
|
703
866
|
if (state.started) return status();
|
|
704
867
|
state.started = true;
|
|
@@ -707,7 +870,6 @@ export function createWhatsAppService({
|
|
|
707
870
|
pruneLogs();
|
|
708
871
|
state.retentionTimer = T.setInterval(pruneLogs, 6 * HOUR);
|
|
709
872
|
state.retentionTimer?.unref?.();
|
|
710
|
-
armAutonomousTimer();
|
|
711
873
|
const cfg = config.read();
|
|
712
874
|
configureHost(cfg);
|
|
713
875
|
if (installerStatus().installed && cfg.enabled && isLinked(cfg)) {
|
|
@@ -718,7 +880,7 @@ export function createWhatsAppService({
|
|
|
718
880
|
}
|
|
719
881
|
async function shutdown({ timeoutMs = 5000 } = {}) {
|
|
720
882
|
state.stopping = true;
|
|
721
|
-
for (const timer of [state.broadcastTimer, state.
|
|
883
|
+
for (const timer of [state.broadcastTimer, state.escalation?.timer, state.link?.timer, state.warningTimer]) if (timer) T.clearTimeout(timer);
|
|
722
884
|
if (state.retentionTimer) T.clearInterval(state.retentionTimer);
|
|
723
885
|
try { state.install?.controller?.abort(); } catch {}
|
|
724
886
|
if (state.link) { state.link.stream.end({ type: 'error', code: 'SHUTTING_DOWN' }); state.link = null; }
|
|
@@ -730,6 +892,7 @@ export function createWhatsAppService({
|
|
|
730
892
|
}
|
|
731
893
|
function killNow() {
|
|
732
894
|
state.stopping = true;
|
|
895
|
+
clearWarningTimer();
|
|
733
896
|
const bridge = state.bridge;
|
|
734
897
|
try { bridge?.endComputerUse?.('process exit'); } catch {}
|
|
735
898
|
state.bridge = null;
|
|
@@ -749,8 +912,8 @@ export function createWhatsAppService({
|
|
|
749
912
|
state.linkOutcome = { state: 'link_expired', at: now() };
|
|
750
913
|
record('state', lastEvent?.code ? `Linking failed (${lastEvent.code})` : 'The link code expired before a phone used it');
|
|
751
914
|
// Nothing linked: stop the host (and Baileys) until the next attempt.
|
|
752
|
-
const r = manager
|
|
753
|
-
Promise.resolve(r).finally(() => { if (!isLinked() && !state.link) Promise.resolve(manager
|
|
915
|
+
const r = manager?.disconnect?.();
|
|
916
|
+
Promise.resolve(r).finally(() => { if (!isLinked() && !state.link) Promise.resolve(manager?.stop?.()).catch(() => {}); }).catch(() => {});
|
|
754
917
|
}
|
|
755
918
|
notify();
|
|
756
919
|
}
|
|
@@ -767,6 +930,7 @@ export function createWhatsAppService({
|
|
|
767
930
|
if (state.link) throw whatsappError(409, 'LINK_BUSY', 'Another SynaBun tab is already linking WhatsApp. Finish or close it first.');
|
|
768
931
|
if (state.install) throw whatsappError(409, 'INSTALL_BUSY', 'The WhatsApp connector is still installing.');
|
|
769
932
|
if (!installerStatus().installed) throw whatsappError(409, 'NOT_INSTALLED', 'Install the WhatsApp connector first.');
|
|
933
|
+
requireManager();
|
|
770
934
|
const cfg = config.read();
|
|
771
935
|
if (isLinked(cfg)) throw whatsappError(409, 'ALREADY_LINKED', 'A WhatsApp account is already linked. Unlink it first.');
|
|
772
936
|
if (body.mode !== undefined) {
|
|
@@ -785,7 +949,7 @@ export function createWhatsAppService({
|
|
|
785
949
|
state.link = null;
|
|
786
950
|
T.clearTimeout(current.timer);
|
|
787
951
|
record('state', 'Linking cancelled (the settings tab closed)');
|
|
788
|
-
Promise.resolve(manager
|
|
952
|
+
Promise.resolve(manager?.disconnect?.()).finally(() => { if (!isLinked() && !state.link) Promise.resolve(manager?.stop?.()).catch(() => {}); }).catch(() => {});
|
|
789
953
|
notify();
|
|
790
954
|
});
|
|
791
955
|
stream.write({ type: 'state', state: 'linking', phase: 'starting', method });
|
|
@@ -814,6 +978,7 @@ export function createWhatsAppService({
|
|
|
814
978
|
async function confirmOwner(body = {}) {
|
|
815
979
|
requireAvailable();
|
|
816
980
|
if (typeof body.accept !== 'boolean') throw whatsappError(400, 'BAD_REQUEST', 'accept must be true or false', 'accept');
|
|
981
|
+
requireManager();
|
|
817
982
|
const result = await manager.confirmOwner(body.accept);
|
|
818
983
|
if (!result?.ok) throw whatsappError(409, result?.code || 'NOT_AWAITING', result?.message ? redactWa(result.message) : 'Nothing is waiting for confirmation.');
|
|
819
984
|
if (!body.accept) {
|
|
@@ -821,7 +986,7 @@ export function createWhatsAppService({
|
|
|
821
986
|
config.write({ owner: null, setup: cfg.setup ? { ...cfg.setup, linkedAt: null } : null });
|
|
822
987
|
record('security', 'You said the linked account is not yours; SynaBun logged it out');
|
|
823
988
|
await dropBridge();
|
|
824
|
-
Promise.resolve(manager
|
|
989
|
+
Promise.resolve(manager?.stop?.()).catch(() => {});
|
|
825
990
|
}
|
|
826
991
|
notify();
|
|
827
992
|
return { ok: true, status: status() };
|
|
@@ -839,6 +1004,7 @@ export function createWhatsAppService({
|
|
|
839
1004
|
if (cfg.mode !== 'dedicated') throw whatsappError(409, 'WRONG_MODE', 'Claim codes are for the second-number mode.');
|
|
840
1005
|
if (conn.state !== 'open') throw whatsappError(409, 'NOT_CONNECTED', 'WhatsApp is not connected.');
|
|
841
1006
|
if (conn.owner?.bound) throw whatsappError(409, 'OWNER_BOUND', 'An owner is already set. Reset it first.');
|
|
1007
|
+
requireManager();
|
|
842
1008
|
const result = await manager.claimStart({ ttlMs: L.claimTtlMs });
|
|
843
1009
|
if (!result?.ok) throw whatsappError(409, result?.code || 'CLAIM_FAILED', result?.message ? redactWa(result.message) : 'Could not start a claim code.');
|
|
844
1010
|
if (state.claim) endClaim({ type: 'cancelled' });
|
|
@@ -849,7 +1015,7 @@ export function createWhatsAppService({
|
|
|
849
1015
|
stream.onAbort(() => {
|
|
850
1016
|
if (state.claim !== current) return;
|
|
851
1017
|
state.claim = null;
|
|
852
|
-
Promise.resolve(manager
|
|
1018
|
+
Promise.resolve(manager?.claimCancel?.()).catch(() => {});
|
|
853
1019
|
notify();
|
|
854
1020
|
});
|
|
855
1021
|
let qrSvg = null;
|
|
@@ -861,6 +1027,7 @@ export function createWhatsAppService({
|
|
|
861
1027
|
async function resetOwner() {
|
|
862
1028
|
requireAvailable();
|
|
863
1029
|
if (config.read().mode !== 'dedicated') throw whatsappError(409, 'WRONG_MODE', 'In "Message yourself" the owner is the linked account. Unlink to link another one.');
|
|
1030
|
+
requireManager();
|
|
864
1031
|
const result = await manager.ownerReset();
|
|
865
1032
|
if (!result?.ok) throw whatsappError(409, result?.code || 'RESET_FAILED', result?.message ? redactWa(result.message) : 'Could not reset the owner.');
|
|
866
1033
|
// The second account stays linked: keep that on record while the owner is unbound.
|
|
@@ -884,7 +1051,7 @@ export function createWhatsAppService({
|
|
|
884
1051
|
(async () => {
|
|
885
1052
|
// The host holds the runtime's files open (Windows locks them): stop it first, restart after.
|
|
886
1053
|
if (hostRunning()) await dropBridge();
|
|
887
|
-
try { await manager
|
|
1054
|
+
try { await manager?.stop(); } catch {}
|
|
888
1055
|
let result;
|
|
889
1056
|
try {
|
|
890
1057
|
result = await installer.install({
|
|
@@ -928,7 +1095,7 @@ export function createWhatsAppService({
|
|
|
928
1095
|
requireAvailable();
|
|
929
1096
|
if (state.install) throw whatsappError(409, 'INSTALL_BUSY', 'The connector is being installed.');
|
|
930
1097
|
if (isLinked()) throw whatsappError(409, 'STILL_LINKED', 'Unlink WhatsApp before removing the connector.');
|
|
931
|
-
try { await manager
|
|
1098
|
+
try { await manager?.stop(); } catch {}
|
|
932
1099
|
const result = await installer.uninstall();
|
|
933
1100
|
state.installCache = null;
|
|
934
1101
|
state.installError = null;
|
|
@@ -986,6 +1153,67 @@ export function createWhatsAppService({
|
|
|
986
1153
|
if (!result?.ok) throw whatsappError(502, result?.code || 'SEND_FAILED', result?.message ? redactWa(result.message) : 'The test message could not be sent.');
|
|
987
1154
|
return { ok: true };
|
|
988
1155
|
}
|
|
1156
|
+
// ── a local file to the owner (whatsapp_send_file; docs/whatsapp.md → Sending files) ──
|
|
1157
|
+
// The HTTP status of each refusal (POST /api/assistant/whatsapp/send-file answers with it).
|
|
1158
|
+
const FILE_STATUS = Object.freeze({
|
|
1159
|
+
BAD_REQUEST: 400, ADDRESS_FORBIDDEN: 400, FILE_FORBIDDEN: 403, FILE_TOO_LARGE: 413, FILE_INVALID: 422,
|
|
1160
|
+
NOT_CONNECTED: 409, PAUSED: 409, NO_OWNER: 409, HELD: 409, THROTTLED: 429, SEND_FAILED: 502, HOST_UNAVAILABLE: 503, TIMEOUT: 504,
|
|
1161
|
+
});
|
|
1162
|
+
const FILE_FIELDS = Object.freeze(['path', 'caption', 'as']);
|
|
1163
|
+
// Refusals whose message is about the file itself (local-file.js): its path, and why.
|
|
1164
|
+
const FILE_REFUSALS = new Set(['FILE_FORBIDDEN', 'FILE_TOO_LARGE', 'FILE_INVALID', 'BAD_REQUEST']);
|
|
1165
|
+
const UNCERTAIN_FILE = 'WhatsApp did not confirm the file: it may or may not have arrived. Do not send it again unless the user asks.';
|
|
1166
|
+
const fileSize = (bytes) => { const n = Number(bytes) || 0; return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)} MiB` : n >= 1024 ? `${Math.round(n / 1024)} KiB` : `${n} bytes`; };
|
|
1167
|
+
/**
|
|
1168
|
+
* Send a local file to the owner, for the WhatsApp conversation `sessionId` (the caller proved it
|
|
1169
|
+
* holds that conversation's brain: lib/assistant-api.js). Only { path, caption?, as? } (anything
|
|
1170
|
+
* else is a 400); only the active WhatsApp session (never an older one, a desktop chat or a
|
|
1171
|
+
* worker: 403 WHATSAPP_SESSION_REQUIRED); only while WhatsApp is on, not paused and connected
|
|
1172
|
+
* (409 PAUSED / NOT_CONNECTED). The host checks and reads the file (local-file.js). Exactly one
|
|
1173
|
+
* request, never retried: a refusal with `uncertain: true` (a timeout) may have been delivered.
|
|
1174
|
+
* Activity records the outcome with the path, never the caption or the bytes.
|
|
1175
|
+
*/
|
|
1176
|
+
async function sendFile(sessionId, fields = {}) {
|
|
1177
|
+
requireAvailable();
|
|
1178
|
+
const body = fields && typeof fields === 'object' && !Array.isArray(fields) ? fields : null;
|
|
1179
|
+
if (!body) throw whatsappError(400, 'BAD_REQUEST', 'Send { path, caption?, as? }.');
|
|
1180
|
+
const extra = Object.keys(body).find((key) => !FILE_FIELDS.includes(key));
|
|
1181
|
+
if (extra !== undefined) throw whatsappError(400, 'BAD_REQUEST', `Unknown field "${extra.slice(0, 40)}": a file send takes path, caption and as only.`, extra.slice(0, 40));
|
|
1182
|
+
if (typeof body.path !== 'string' || !body.path.trim() || body.path.length > LIMITS.MAX_FILE_PATH || /[\u0000-\u001f\u007f]/.test(body.path)) throw whatsappError(400, 'BAD_REQUEST', `path must be the absolute path of a file on this computer (at most ${LIMITS.MAX_FILE_PATH} characters, no control characters).`, 'path');
|
|
1183
|
+
if (body.caption !== undefined && body.caption !== null && (typeof body.caption !== 'string' || body.caption.length > LIMITS.MAX_FILE_CAPTION)) throw whatsappError(400, 'BAD_REQUEST', `caption must be text of at most ${LIMITS.MAX_FILE_CAPTION} characters.`, 'caption');
|
|
1184
|
+
if (body.as !== undefined && body.as !== null && !FILE_SEND_AS.includes(body.as)) throw whatsappError(400, 'BAD_REQUEST', `as must be one of ${FILE_SEND_AS.join(', ')}.`, 'as');
|
|
1185
|
+
const cfg = config.read();
|
|
1186
|
+
const active = bridgeStatus()?.sessionId || cfg.sessionId || null;
|
|
1187
|
+
let channel = null;
|
|
1188
|
+
try { channel = sessionId ? getRuntime()?.sessionChannel?.(String(sessionId)) || null : null; } catch { channel = null; }
|
|
1189
|
+
if (!sessionId || !active || String(sessionId) !== String(active) || channel !== 'whatsapp') {
|
|
1190
|
+
throw whatsappError(403, 'WHATSAPP_SESSION_REQUIRED', 'Only the active WhatsApp conversation sends files to the phone.');
|
|
1191
|
+
}
|
|
1192
|
+
if (!cfg.enabled || cfg.paused || state.stopping) throw whatsappError(409, 'PAUSED', 'WhatsApp is turned off or paused on this computer: nothing was sent.');
|
|
1193
|
+
requireManager();
|
|
1194
|
+
if (!connectedNow()) throw whatsappError(409, 'NOT_CONNECTED', 'WhatsApp is not connected: nothing was sent.');
|
|
1195
|
+
const request = { path: body.path };
|
|
1196
|
+
if (typeof body.caption === 'string') request.caption = body.caption;
|
|
1197
|
+
if (typeof body.as === 'string') request.as = body.as;
|
|
1198
|
+
let result;
|
|
1199
|
+
try { result = await manager.sendFile(request); } catch (error) { result = { ok: false, code: 'SEND_FAILED', message: error?.message || String(error) }; }
|
|
1200
|
+
if (result?.ok) {
|
|
1201
|
+
record('reply', `Sent a file to your phone: ${result.type}${result.fallback ? ' (as a document: over 16 MiB)' : ''}, ${fileSize(result.size)}:`, null, { filePath: result.path });
|
|
1202
|
+
notify();
|
|
1203
|
+
return { ok: true, id: result.id ?? null, type: result.type, mimetype: result.mimetype, size: result.size, fileName: result.fileName, path: result.path, fallback: result.fallback === true };
|
|
1204
|
+
}
|
|
1205
|
+
const code = String(result?.code || 'SEND_FAILED');
|
|
1206
|
+
const uncertain = result?.uncertain === true;
|
|
1207
|
+
record('error', `A file was not sent (${code}${uncertain ? '; delivery unknown' : ''}):`, null, { filePath: body.path, requested: true });
|
|
1208
|
+
notify();
|
|
1209
|
+
// A file's own refusal keeps its path readable (redactFilePath: numbers, JIDs and credentials still hidden).
|
|
1210
|
+
const reason = result?.message ? (FILE_REFUSALS.has(code) ? redactFilePath(result.message) : redactWa(result.message)) : 'WhatsApp refused the file.';
|
|
1211
|
+
const error = whatsappError(FILE_STATUS[code] || 502, code, uncertain ? UNCERTAIN_FILE : reason);
|
|
1212
|
+
if (uncertain) error.uncertain = true;
|
|
1213
|
+
if (Number.isFinite(result?.retryAfterMs)) error.retryAfterMs = result.retryAfterMs;
|
|
1214
|
+
throw error;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
989
1217
|
function pause({ by = 'desktop' } = {}) {
|
|
990
1218
|
requireAvailable();
|
|
991
1219
|
// An agent's pause counts as the desktop's: the phone cannot undo it.
|
|
@@ -1012,6 +1240,7 @@ export function createWhatsAppService({
|
|
|
1012
1240
|
requireAvailable();
|
|
1013
1241
|
if (!installerStatus().installed) throw whatsappError(409, 'NOT_INSTALLED', 'Install the WhatsApp connector first.');
|
|
1014
1242
|
if (!isLinked()) throw whatsappError(409, 'NOT_LINKED', 'No WhatsApp account is linked.');
|
|
1243
|
+
requireManager();
|
|
1015
1244
|
const cfg = config.read();
|
|
1016
1245
|
if (!cfg.enabled) config.write({ enabled: true });
|
|
1017
1246
|
state.fatal = null;
|
|
@@ -1024,13 +1253,13 @@ export function createWhatsAppService({
|
|
|
1024
1253
|
if (state.link) finishLink('failed', { type: 'error', code: 'UNLINKED' });
|
|
1025
1254
|
endClaim({ type: 'cancelled' });
|
|
1026
1255
|
endEscalation('unlinked');
|
|
1027
|
-
T.clearTimeout(state.autonomousTimer);
|
|
1028
|
-
state.autonomousTimer = null;
|
|
1029
1256
|
await dropBridge();
|
|
1030
1257
|
let loggedOut = false;
|
|
1031
1258
|
let code = null;
|
|
1032
1259
|
if (manager && installerStatus().installed) {
|
|
1033
|
-
|
|
1260
|
+
// A host between restarts ('backoff') is started now: the logout must not wait out a respawn of up to 5 minutes,
|
|
1261
|
+
// and the link is not reconnected first (resume: false drops the run intent).
|
|
1262
|
+
if (managerStatus().host.state !== 'running') { const started = await manager.start(prefsOf(config.read()), { resume: false }); if (!started?.ok) code = started?.code || 'HOST_UNAVAILABLE'; }
|
|
1034
1263
|
if (!code) {
|
|
1035
1264
|
const result = await manager.logout();
|
|
1036
1265
|
loggedOut = !!result?.ok;
|
|
@@ -1045,6 +1274,7 @@ export function createWhatsAppService({
|
|
|
1045
1274
|
state.fatal = null;
|
|
1046
1275
|
state.lastConn = null;
|
|
1047
1276
|
state.wasConnected = false;
|
|
1277
|
+
clearWarning();
|
|
1048
1278
|
return { loggedOut, code };
|
|
1049
1279
|
}
|
|
1050
1280
|
async function unlink() {
|
|
@@ -1092,7 +1322,8 @@ export function createWhatsAppService({
|
|
|
1092
1322
|
const userView = (cfg) => pick(cfg, USER_FIELDS);
|
|
1093
1323
|
function getConfig() {
|
|
1094
1324
|
const cfg = config.read();
|
|
1095
|
-
|
|
1325
|
+
// levelExpiresAt: Autonomous has no time limit (always null, kept for older readers).
|
|
1326
|
+
return { ok: true, config: userView(cfg), version: cfg.version, level: currentLevel(cfg), levelExpiresAt: null, escalation: escalationView() };
|
|
1096
1327
|
}
|
|
1097
1328
|
function escalationView() {
|
|
1098
1329
|
const esc = state.escalation;
|
|
@@ -1113,7 +1344,7 @@ export function createWhatsAppService({
|
|
|
1113
1344
|
patch.brain = checked.brain;
|
|
1114
1345
|
}
|
|
1115
1346
|
const before = config.read();
|
|
1116
|
-
const wantsAutonomous = patch?.level === 'autonomous' && currentLevel(before
|
|
1347
|
+
const wantsAutonomous = patch?.level === 'autonomous' && currentLevel(before) !== 'autonomous';
|
|
1117
1348
|
if (wantsAutonomous && body.confirmEscalation === true && !connectedNow()) {
|
|
1118
1349
|
throw whatsappError(409, 'PHONE_REQUIRED', 'Connect WhatsApp first: your phone confirms Autonomous with an ALLOW code.', 'level');
|
|
1119
1350
|
}
|
|
@@ -1122,16 +1353,29 @@ export function createWhatsAppService({
|
|
|
1122
1353
|
let pending = null;
|
|
1123
1354
|
if (result.pending) pending = startEscalation();
|
|
1124
1355
|
else if (patch && Object.hasOwn(patch, 'level') && patch.level !== 'autonomous') endEscalation('level lowered');
|
|
1125
|
-
if (before.level !== after.level
|
|
1126
|
-
armAutonomousTimer();
|
|
1356
|
+
if (before.level !== after.level) {
|
|
1127
1357
|
record('security', `Level set to ${after.level === 'read-only' ? 'Read-only' : after.level === 'ask' ? 'Ask on my phone' : 'Autonomous'} from this computer`);
|
|
1128
1358
|
}
|
|
1129
1359
|
if (before.computerUse !== after.computerUse) record('security', after.computerUse ? 'Computer use from WhatsApp turned on from this computer' : 'Computer use from WhatsApp turned off from this computer');
|
|
1130
1360
|
if (before.activityText !== after.activityText) record('settings', after.activityText ? 'Message text is now kept in this activity log' : 'Message text is no longer kept in this activity log');
|
|
1131
1361
|
if (before.selfTrigger !== after.selfTrigger) record('settings', after.selfTrigger === 'prefix' ? 'Only messages that start with sb go to SynaBun now' : 'Every message in "Message yourself" goes to SynaBun now');
|
|
1362
|
+
// Voice notes: the host gets the new settings at once (it transcribes), and Settings looks at the tools again.
|
|
1363
|
+
const transcriptionChanged = TRANSCRIPTION_FIELDS.some((key) => before[key] !== after[key]);
|
|
1364
|
+
if (transcriptionChanged) {
|
|
1365
|
+
state.transcription = null;
|
|
1366
|
+
if (before.transcriptionEnabled !== after.transcriptionEnabled) record('settings', after.transcriptionEnabled ? 'Voice notes are transcribed on this computer now' : 'Voice notes are no longer transcribed');
|
|
1367
|
+
if (TRANSCRIPTION_FIELDS.some((key) => key !== 'transcriptionEnabled' && before[key] !== after[key])) record('settings', 'Voice note settings changed (language or program paths)');
|
|
1368
|
+
configureHost(after);
|
|
1369
|
+
}
|
|
1132
1370
|
if (JSON.stringify(before.brain) !== JSON.stringify(after.brain)) {
|
|
1133
1371
|
record('settings', after.brain ? `WhatsApp now runs on ${providerLabel(after.brain.provider)} / ${after.brain.model}${after.brain.effort ? ` (${after.brain.effort})` : ''}, from the next message` : 'WhatsApp now runs on the same brain as the Assistant, from the next message');
|
|
1134
1372
|
}
|
|
1373
|
+
// Off stops the host and every retry (the link stays); on resumes the saved session. Pause is separate.
|
|
1374
|
+
if (before.enabled !== after.enabled) {
|
|
1375
|
+
record('settings', after.enabled ? 'WhatsApp turned on from this computer' : 'WhatsApp turned off from this computer: disconnected, and no reconnect until it is turned on again');
|
|
1376
|
+
if (!after.enabled) await stopForDisable();
|
|
1377
|
+
else if (state.started && !state.stopping && manager && installerStatus().installed && isLinked(after)) await run().catch(() => {});
|
|
1378
|
+
}
|
|
1135
1379
|
refreshBridge();
|
|
1136
1380
|
notify();
|
|
1137
1381
|
return { ok: true, config: userView(after), version: after.version, pending };
|
|
@@ -1192,7 +1436,7 @@ export function createWhatsAppService({
|
|
|
1192
1436
|
* when it is Codex or OpenCode and the level asks for more than read-only.
|
|
1193
1437
|
*/
|
|
1194
1438
|
function brainLimitView(cfg, sessionId) {
|
|
1195
|
-
if (currentLevel(cfg
|
|
1439
|
+
if (currentLevel(cfg) === 'read-only') return null;
|
|
1196
1440
|
let provider = null;
|
|
1197
1441
|
try { provider = sessionId ? getRuntime()?.getSession?.(sessionId, { transcript: false })?.brain?.provider || null : null; } catch { provider = null; }
|
|
1198
1442
|
if (!provider) provider = brainChoice(cfg).effective?.provider || null;
|
|
@@ -1204,6 +1448,7 @@ export function createWhatsAppService({
|
|
|
1204
1448
|
if (!fakeEnabled()) throw whatsappError(404, 'NOT_FOUND', 'Not found');
|
|
1205
1449
|
const action = String(body.action || '');
|
|
1206
1450
|
const cfg = config.read();
|
|
1451
|
+
requireManager();
|
|
1207
1452
|
const call = async (name, args = {}) => {
|
|
1208
1453
|
const result = await manager.fake(name, args);
|
|
1209
1454
|
if (result?.ok === false) throw whatsappError(409, result.code || 'FAKE_FAILED', result.message || `fake ${name} failed`);
|
|
@@ -1241,7 +1486,7 @@ export function createWhatsAppService({
|
|
|
1241
1486
|
const tab = tabState();
|
|
1242
1487
|
const { conn, host, runtime } = managerStatus();
|
|
1243
1488
|
const inst = installerStatus();
|
|
1244
|
-
const level = currentLevel(cfg
|
|
1489
|
+
const level = currentLevel(cfg);
|
|
1245
1490
|
const bridge = bridgeStatus();
|
|
1246
1491
|
const sessionId = bridge?.sessionId || cfg.sessionId || null;
|
|
1247
1492
|
let confirm = null;
|
|
@@ -1257,7 +1502,8 @@ export function createWhatsAppService({
|
|
|
1257
1502
|
...tab,
|
|
1258
1503
|
mode: cfg.mode,
|
|
1259
1504
|
level,
|
|
1260
|
-
|
|
1505
|
+
// Autonomous has no time limit: kept for older readers, always null.
|
|
1506
|
+
levelExpiresAt: null,
|
|
1261
1507
|
enabled: cfg.enabled,
|
|
1262
1508
|
paused: cfg.paused,
|
|
1263
1509
|
pausedBy: cfg.pausedBy,
|
|
@@ -1268,7 +1514,7 @@ export function createWhatsAppService({
|
|
|
1268
1514
|
installedAt: inst.installedAt ?? null, approxSizeMB: inst.approxSizeMB ?? null, stage: state.install?.stage || null,
|
|
1269
1515
|
errorCode: state.installError?.code || null, errorMessage: state.installError?.message || null, runtime: runtime.loaded ? { version: runtime.version, fake: runtime.fake } : null,
|
|
1270
1516
|
},
|
|
1271
|
-
host: { state: host.state, restarts: host.restarts || 0 },
|
|
1517
|
+
host: { state: host.state, restarts: host.restarts || 0, retryInMs: Number.isFinite(host.retryInMs) ? host.retryInMs : null },
|
|
1272
1518
|
account: conn.me ? { masked: conn.me.masked, name: conn.me.name ?? null } : null,
|
|
1273
1519
|
owner,
|
|
1274
1520
|
confirm,
|
|
@@ -1277,6 +1523,10 @@ export function createWhatsAppService({
|
|
|
1277
1523
|
brainChoice: brainChoice(cfg),
|
|
1278
1524
|
bridge: bridge ? { running: !!bridge.running, queued: bridge.queued || 0, pendingCards: bridge.pendingCards || 0, lastReplyAt: bridge.lastReplyAt || null } : null,
|
|
1279
1525
|
connection: tab.state === 'reconnecting' ? { attempt: state.reconnectAttempt, nextRetryAt: Number.isFinite(conn.retryInMs) ? state.lastStateAt + conn.retryInMs : null } : null,
|
|
1526
|
+
// 'replaced' (440) | 'forbidden' (403) while the link retries one of them, until it is stable again.
|
|
1527
|
+
warning: state.warning?.kind || null,
|
|
1528
|
+
// Voice notes: on, and whether whisper-cli, the model and FFmpeg are found (what is missing when not).
|
|
1529
|
+
transcription: transcriptionView(cfg),
|
|
1280
1530
|
counters: { ...(conn.counters || {}) },
|
|
1281
1531
|
config: userView(cfg),
|
|
1282
1532
|
version: cfg.version,
|
|
@@ -1299,6 +1549,8 @@ export function createWhatsAppService({
|
|
|
1299
1549
|
return {
|
|
1300
1550
|
router, start, shutdown, killNow, status,
|
|
1301
1551
|
toSyncPayload,
|
|
1552
|
+
// Not a /api/whatsapp route: POST /api/assistant/whatsapp/send-file calls it for the brain it identified.
|
|
1553
|
+
sendFile,
|
|
1302
1554
|
_internals: { state, config, ops, activity, record, tabState, handleAllow, flushBroadcast, manager, installer, paths, brainChoice },
|
|
1303
1555
|
};
|
|
1304
1556
|
}
|