synabun 2.0.5 → 2.0.7
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 +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- 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/packaged-runtime.js +138 -11
- 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/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/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/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 +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.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/tui.js +2 -1
- 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 +2 -2
- package/mcp-server/package.json +1 -1
- 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/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- 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 +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -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.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- 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 +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -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 +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- 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 +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- 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 +95 -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-title-generator.js +4 -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 +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- 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 +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- 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 +99 -17
- 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 +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -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-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- 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 +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -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 +6 -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 +594 -5
- 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/server.js +1219 -608
- package/package.json +4 -1
- package/setup.js +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
# SynaBun v.2.0.7
|
|
2
|
+
|
|
3
|
+
2.0.7 brings the SynaBun desktop app for macOS, Windows and Linux, with updates that install themselves and a Windows installer that shows every step. SynaBun now finds every installed Claude Code, Codex and OpenCode the same way and installs a missing one with one click, you can sign in from the Claude and Codex panels, and the WhatsApp Assistant can send you files and understand voice notes.
|
|
4
|
+
|
|
5
|
+
## ✨ New
|
|
6
|
+
|
|
7
|
+
- **SynaBun desktop app** — The installers are now a real desktop app: SynaBun opens in its own window on macOS (Apple Silicon and Intel), Windows x64 and Linux x64, around the same server, runtime and interface as before. The window has SynaBun's own title bar instead of the system's: the window buttons sit at its right end on Windows and Linux, where a button at the left opens the menu, and the traffic lights sit inside it on macOS. Closing the window keeps SynaBun running; the app menu, the Dock or the tray bring it back. npm and Git installs are unchanged.
|
|
8
|
+
- **Updates that install themselves** — The desktop app checks the public releases shortly after it starts and every few hours. A newer version downloads at once. When SynaBun is idle, a one-minute notice with **Later** counts down, then the update installs, the window reopens as it was and a notice says what it updated to. While work is running it waits, and it also installs when you quit or at the next start. **Install now** is in the update dialog and the menus. If an update fails, the version you have keeps running, you get a link to download the new one yourself, and it tries again later.
|
|
9
|
+
- **A Windows installer that shows every step** — The installer lists each step as it goes, with the seconds since it started: the check for a running SynaBun, the unpacking with its progress, each part as it is put in place, the shortcuts and the **Settings → Apps** entry. The uninstaller lists its steps too, and the whole list is saved to a log you can open from the last page. If SynaBun is running, the installer asks once and closes it for you, letting the server shut down cleanly first; it never touches a SynaBun installed with npm or Git. Installing is faster: the files are unpacked straight into place, and the app carries about 6,600 fewer files. An install counts as finished only when every file is in place, and an update that cannot finish leaves the version you have running. Updates from the app stay silent.
|
|
10
|
+
- **See what the desktop app is doing while it starts** — **Details** on the start screen shows a live log of the start: the checks, the browser download with its progress, the build and the server's first lines. It opens by itself on the first start and when something fails, and keeps the real error. In the setup wizard, **Details** under Environment Check and Install & Build show the commands that ran with their answers, and what the app came with.
|
|
11
|
+
- **npm and npx come with the desktop app** — They work even when your computer has no Node.js. Your own npm and tools always come first; the bundled ones are only a fallback, and their global installs go to a folder in SynaBun's data folder instead of into the app, unless you set an npm prefix of your own.
|
|
12
|
+
- **Git Bash for Claude Code on Windows** — On a Windows PC without Git for Windows, Claude Code ran its hooks in PowerShell, where SynaBun's hooks do not work. SynaBun now brings its own unchanged copy of Git for Windows and uses it only when your PC has none: the Windows installer carries it, and an npm install on Windows downloads it once and checks it against a pinned checksum before unpacking it. A Git you installed yourself always wins, and **Settings → AI connections → Executable paths** shows which one Claude Code gets.
|
|
13
|
+
- **Settings → About** — A new last page in Settings shows your version and how SynaBun is installed, with **Check for updates** and **Copy info** (version, install type, Node.js, SQLite, the desktop app's Electron and Chromium, your system and the dependency versions; no paths, ports, keys or settings). It also has the project's links, system details and every dependency with its version and license.
|
|
14
|
+
- **SynaBun finds your Claude Code, Codex and OpenCode** — The side panels, Settings and every launcher find each CLI the same way: the path you set in Settings first, then your PATH as a new terminal sees it, then the folders the official installers use. A CLI that is found but does not start shows its real error instead of "not installed".
|
|
15
|
+
- **One-click install of a missing CLI** — When Claude Code, Codex or OpenCode is not installed, its panel shows a banner with **Install**, which runs the vendor's official installer for your system and shows its progress, next to the manual command and the install guide. The OpenCode panel gets this banner too. **Add to PATH** makes an installed CLI available to new terminals.
|
|
16
|
+
- **Sign in from the Claude and Codex panels** — `/login` and the new account button open a sign-in card that drives each CLI's own sign-in: open the sign-in page, finish in your browser, and the panel shows who is signed in. Codex also offers a device code. Tokens stay with Claude Code and Codex, SynaBun stores none, and sign-in cards are never saved into stored chats.
|
|
17
|
+
- **Auto · most usage left** — Claude Code and Codex routes and the composer's model menu can pick the subscription account with the most usage left. Usage windows show beside each account, and an exhausted account falls back to another one of the same provider.
|
|
18
|
+
- **The WhatsApp Assistant can send you files** — Ask for a file on your computer and the Assistant sends it to your WhatsApp chat as a real attachment, with an optional caption: pictures, videos and audio as such, anything else as a document with its name. Files go only to your own chat, files over 100 MB are refused, and SynaBun's own data, your WhatsApp session, your AI tools' settings and keys, and system folders are never sent, whatever path leads to them.
|
|
19
|
+
- **WhatsApp voice notes** — Voice notes and audio messages can now be transcribed on your computer and reach the Assistant as text. Turn on **Transcribe voice notes** in **Settings → Messages → WhatsApp**; it uses your own FFmpeg and whisper.cpp with a model you provide, and no audio leaves your computer. The status line says what is missing. A transcribed note is never taken as a command or an approval, and typed commands such as `/stop` never wait behind one.
|
|
20
|
+
- **Language and showcase addresses** — Adding `?lang=pt-BR` to the interface's address opens it in that language for that page only. `?showcase` opens it without saving any change to your layout and blurs your memories, sessions, transcripts, terminal output and other personal content, for screenshots and recordings.
|
|
21
|
+
|
|
22
|
+
## 🛠 Fixed
|
|
23
|
+
|
|
24
|
+
- **A named Claude profile could run as another account** — A side-panel tab on a named Claude profile could run with the credentials SynaBun itself was started with. It now runs only under its own profile.
|
|
25
|
+
- **Codex could start without SynaBun's tools** — The panels, the Assistant, workers, schedules and loops now register SynaBun's MCP connection with Codex when it is missing, without replacing one you connected yourself.
|
|
26
|
+
- **Terminals did not see newly installed tools** — Every new terminal reads the current system PATH, so a CLI installed after SynaBun started is found.
|
|
27
|
+
- **The MCP server reported version 2.0.0** — It now reports SynaBun's real version.
|
|
28
|
+
- **The WhatsApp link stopped after a connection error** — The link no longer gives up on its own after a stream or connection error from WhatsApp, a burst of reconnects or repeated restarts of the connector. It keeps retrying with your saved session and recovers without **Reconnect**, and stops only when WhatsApp says the session is no longer valid.
|
|
29
|
+
- **Answers to WhatsApp got lost when a task moved to another model** — When the Assistant continued a task you sent from WhatsApp on another model, its answer did not reach your phone. It now comes back once to the chat that started it.
|
|
30
|
+
- **The memory search sat under the menu** — In the Map view, above all in Portuguese, the search field was drawn under the menu labels and could not be used. It now has its own place in the bar at every width and in every language, and turns into a search icon when there is little room.
|
|
31
|
+
|
|
32
|
+
## ⚙️ Changed
|
|
33
|
+
|
|
34
|
+
- **One Assistant budget per session** — Five separate caps are replaced by one budget per session, or **No limit**. The Budget chip next to Project sets it for a new session, and the spend chip, the Budget menu and `/budget` show how it is shared between Claude Code, Codex and OpenCode and what is left. When one provider's share runs out, the rest is rebalanced.
|
|
35
|
+
- **The Assistant mascot follows Project and Budget** — It looks at the Project and Budget chips you point at, and reacts once when a budget is saved.
|
|
36
|
+
- **A dispatched run's time limit counts work time only** — The limit of a run the Assistant dispatches now runs only while the worker is working. Time in the queue, idle time between follow-ups and time waiting for your answer to a permission or a question no longer use it up, so a late follow-up is not cut off midway. The limit is never below 20 minutes.
|
|
37
|
+
- **WhatsApp Autonomous stays on until you turn it off** — Autonomous no longer ends after 8 hours: it stays on until you lower it in Settings. `/pause` pauses messages and `/stop` stops the current task, and neither changes the level. A level that was turned on under the old 8-hour rule comes back as Ask after the update.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
1
41
|
# SynaBun v.2.0.5
|
|
2
42
|
|
|
3
43
|
2.0.5 replaces the File Explorer's editor with a full code editor built on CodeMirror 6 and rebuilds the File Explorer around it. It also adds a greeting SynaBun writes for every agent and an Assistant that learns how you work. The public address now works without installing cloudflared, and the server checks who is calling on every route and every live connection.
|
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ Your AI tool ── MCP ──▶ SynaBun MCP server ──▶ memory.db (SQLite
|
|
|
98
98
|
- **Workspace.** Terminals, a file explorer and a whiteboard next to your memories.
|
|
99
99
|
- **Settings you can search**, in English and Brazilian Portuguese.
|
|
100
100
|
- **A workspace tour** behind the **?** button.
|
|
101
|
+
- **Showcase mode for screenshots.** Open the interface with `?showcase=1` and it loads your layout without saving any change to it, and blurs your own content: memories, session titles, transcripts, terminal output, account names and anything that looks private (emails, home folder paths, ids, keys, phone numbers, the names of your other projects). `?lang=en` or `?lang=pt-BR` sets the language for that page only. Map labels drawn on the canvas cannot be blurred one by one, and the whiteboard and memory positions you drag still save.
|
|
101
102
|
|
|
102
103
|
### Coding agents
|
|
103
104
|
|
|
@@ -158,7 +159,7 @@ Everything SynaBun stores is in one folder outside the app: `~/.synabun` on macO
|
|
|
158
159
|
|
|
159
160
|
## Links
|
|
160
161
|
|
|
161
|
-
- [Changelog](./CHANGELOG.md): what is new in 2.0.
|
|
162
|
+
- [Changelog](./CHANGELOG.md): what is new in 2.0.7 and before
|
|
162
163
|
- [Security](./SECURITY.md): security model and how to report a vulnerability
|
|
163
164
|
- [Contributing](./CONTRIBUTING.md): bug reports and feature requests are welcome as [issues](https://github.com/danilokhury/Synabun/issues); pull requests are not accepted
|
|
164
165
|
- [Website](https://synabun.ai), [docs](https://synabun.ai/docs) and [blog](https://synabun.ai/blog)
|
package/THIRD-PARTY-LICENSES.md
CHANGED
|
@@ -132,12 +132,25 @@ computer, so no QR payload leaves SynaBun.
|
|
|
132
132
|
| Program | Version | Delivery | License | Project |
|
|
133
133
|
| --- | --- | --- | --- | --- |
|
|
134
134
|
| cloudflared | 2026.10.0 (pinned in `neural-interface/lib/cloudflared/pins.json` with the SHA-256 of each release file and of the binary inside it) | Cloudflare's own release binary, unmodified. The application installers carry it; an npm or Git install downloads the same release file from Cloudflare's GitHub releases the first time a tunnel is started and checks it against the pinned SHA-256 before it runs | Apache-2.0 | [GitHub](https://github.com/cloudflare/cloudflared) |
|
|
135
|
+
| Git for Windows | 2.56.0.2 (`v2.56.0.windows.2`, pinned in `neural-interface/lib/git-bash/pins.json` with the SHA-256 of its release file and the digest of the tree it holds) | Git for Windows' own release file `Git-2.56.0.2-64-bit.tar.bz2`, unmodified but for five links into `/proc` it leaves out. Windows only: the Windows x64 installer carries it; an npm install on Windows that finds no Git Bash downloads the same release file from Git for Windows' GitHub releases and checks it against the pinned SHA-256 before it is unpacked. macOS and Linux builds carry none | GPL-2.0-only for Git (some of its parts LGPL-2.1); the MSYS2 and MinGW programs and libraries it is built with carry their own licenses (GPL, LGPL, BSD, MIT, Apache-2.0 and others) | [GitHub](https://github.com/git-for-windows/git) |
|
|
135
136
|
|
|
136
137
|
cloudflared opens the Cloudflare quick tunnel behind the public address
|
|
137
138
|
(invite links and remote MCP). It runs as a separate process, started only
|
|
138
139
|
when the user starts a tunnel. Using cloudflared and the tunnel service is
|
|
139
140
|
subject to Cloudflare's own terms.
|
|
140
141
|
|
|
142
|
+
Git for Windows provides the Git Bash and Unix tools Claude Code runs its
|
|
143
|
+
Bash tool and hooks with on Windows, used only on a PC that has no Git for
|
|
144
|
+
Windows of its own. Claude Code starts it; SynaBun itself uses only its git,
|
|
145
|
+
for its own git features on such a PC. Copyright (c) Linus Torvalds and the
|
|
146
|
+
Git contributors; the Git for Windows project and its contributors; the
|
|
147
|
+
authors of each component. The copy carries `LICENSE.txt` (Git's `COPYING`
|
|
148
|
+
and `LGPL-2.1` of the tag) and `SOURCE.txt` (where the source is: Git at the
|
|
149
|
+
pinned tag, https://github.com/git-for-windows/git/tree/v2.56.0.windows.2,
|
|
150
|
+
and the release's "Source code" archives; the other components at
|
|
151
|
+
https://github.com/git-for-windows). The license of each component is in the
|
|
152
|
+
copy's `usr/share/licenses`, `ucrt64/share/licenses` and `ucrt64/doc`.
|
|
153
|
+
|
|
141
154
|
## Installed On Demand (Not Distributed)
|
|
142
155
|
|
|
143
156
|
The WhatsApp Link's connector is not part of SynaBun and is not in any SynaBun
|
package/docs/whatsapp.md
CHANGED
|
@@ -17,6 +17,7 @@ It is free: SynaBun links to WhatsApp the way WhatsApp Web does, as a **linked d
|
|
|
17
17
|
|
|
18
18
|
**Message yourself** links *your* account. SynaBun reads only your own "Message yourself" chat; every other chat, group and contact is ignored. Replies start with `SynaBun:` (change it under *Reply label*). Only messages typed on the phone count: what you type into "Message yourself" from WhatsApp Web or Desktop is ignored. With *In "Message yourself" → Only messages that start with sb* you can keep using the chat for notes: only messages that start with `sb ` or `sb:` reach SynaBun (the prefix is removed; a bare `sb` works as a picture caption), plus commands (`/stop`, `/status` …), the `ALLOW` answer and replies that quote one of SynaBun's messages. Everything else is a note to self and is dropped without being logged. The service applies this filter; the connector's own prefix filter stays off (`selfTrigger: 'all'` to the host) because it would also drop commands and card answers.
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
**A second number** links a spare WhatsApp account (a second SIM, or WhatsApp Business on another number). You chat with it like a contact, so replies notify you, and your own account is never linked. Whoever sends the one-time **claim code** first becomes the owner; nobody else is ever answered.
|
|
21
22
|
|
|
22
23
|
## Setup
|
|
@@ -38,7 +39,7 @@ Settings → Messages → WhatsApp → **Safety** decides what a message from th
|
|
|
38
39
|
|---|---|
|
|
39
40
|
| **Read-only** | Answers, research and plans only. Plan mode everywhere, no agents; a plan can be approved on the computer only. |
|
|
40
41
|
| **Ask on my phone** (default) | Before anything that would change files, run a command or start an agent, SynaBun asks on WhatsApp as a yes / no and acts only on your **yes** (once). Any other message approves nothing and the conversation carries on (see [Questions and approvals](#questions-and-approvals)). Where a task runs is asked the same way, and that answer approves exactly one agent: a second one is asked again. Agents started from WhatsApp get no computer use and run in a registered project folder. |
|
|
41
|
-
| **Autonomous** | Acts without asking, like the Assistant on the computer
|
|
42
|
+
| **Autonomous** | Acts without asking, like the Assistant on the computer. It **stays on until you turn it off** in Settings → Messages → WhatsApp → **Safety** by choosing Ask on my phone or Read-only, and it survives restarts. Autonomous can read any file your computer account can, including SynaBun's WhatsApp login. Use it only with a locked phone, ideally on a second number. |
|
|
42
43
|
|
|
43
44
|
**Ask and Autonomous need a Claude brain.** Which model runs the conversation is set under Conversation → **Model** (see [The model](#the-model)); the default is the Assistant panel's last-used brain. On a Codex or OpenCode brain the conversation runs **Read-only**, whatever the level says: Codex cannot ask before ordinary commands, and OpenCode keeps its own permissions. Codex runs in its read-only sandbox with the network off. OpenCode runs its plan agent, with `webfetch`, `websearch` and `codesearch` refused by SynaBun's gate plugin; if that plugin does not load, the conversation does not run. The phone is told once ("This conversation runs read-only because its brain is Codex; switch the WhatsApp brain to Claude for Ask/Autonomous.") and Safety shows the same warning.
|
|
44
45
|
|
|
@@ -47,6 +48,8 @@ Turning on Autonomous takes two steps so that neither a stolen phone nor a local
|
|
|
47
48
|
1. Click **Autonomous** in Settings, then click it again (an armed second click). Settings shows a 4-digit code, and the phone gets a message saying Autonomous was requested (without the code).
|
|
48
49
|
2. Within **5 minutes**, send `ALLOW 1234` (that code) from the phone. Three wrong codes cancel it. A forwarded message never counts.
|
|
49
50
|
|
|
51
|
+
There is no time limit after that: the phone's confirmation says Autonomous stays on until it is turned off in Settings → WhatsApp. `/pause` pauses acting on your messages and `/stop` stops the current task; neither changes the level. An Autonomous turned on by SynaBun 2.0.5 or earlier (which ended by itself after 8 hours) is not carried over: after the update it reads as Ask, once, and Activity says so ("Autonomous from an earlier version ended; turn it on again to keep it on without a time limit"). Turn it on again to keep it on.
|
|
52
|
+
|
|
50
53
|
Lowering the level applies at once, and so does switching the conversation's brain to Codex or OpenCode. At every level:
|
|
51
54
|
|
|
52
55
|
- Computer use (the Assistant controlling this Mac) is off unless you turned it on at this computer, and then only as the level allows: see [Computer use](#computer-use).
|
|
@@ -67,7 +70,7 @@ With the switch on, the level decides:
|
|
|
67
70
|
|---|---|
|
|
68
71
|
| **Read-only** | Off. |
|
|
69
72
|
| **Ask on my phone** | **One yes per task.** At Ask every task is already asked where it runs; for a task done on the Mac that question is the one about the Mac too: *"For "Tidy the desktop", want me to do it on your Mac, here with Sonnet? I'll control the screen until this task is done. (yes / no)"*. A plain **yes** from your phone turns computer use on for that task; there is no approval per click and no second question. `no` drops the task. |
|
|
70
|
-
| **Autonomous** | Works without asking while
|
|
73
|
+
| **Autonomous** | Works without asking while Autonomous is enabled, **in a task your own plain message started**. Every other turn asks once on your phone, as at Ask: a turn SynaBun starts itself (an agent's result, a route decision, a follow-up on another model, background work), a turn typed into the conversation on the computer, and a message with forwarded or quoted text or a picture. What started the turn decides, not what was said before it. |
|
|
71
74
|
|
|
72
75
|
Off as well, whatever the level: while WhatsApp is paused, in a conversation whose brain is Codex or OpenCode (it runs read-only), and when computer use is not set up on this Mac (the *Computer* switch in the Assistant panel runs the one-time setup).
|
|
73
76
|
|
|
@@ -91,15 +94,16 @@ The approval is bound to the card you saw: SynaBun records on the card, when it
|
|
|
91
94
|
- you send **another message** from your phone while an approved task runs: the approval ends at once, before that message is queued; the task goes on without the Mac and your message is delivered after it as before. (A task that runs unasked at Autonomous holds no approval to end: it keeps running, and `/stop` stops it.)
|
|
92
95
|
- you send `/stop`, or press Stop in SynaBun;
|
|
93
96
|
- WhatsApp is paused (`/pause`, **Pause now**, an agent's pause);
|
|
94
|
-
- the level changes
|
|
97
|
+
- the level changes;
|
|
95
98
|
- the switch is turned off;
|
|
99
|
+
- WhatsApp is turned off on this computer (`enabled: false`, which also stops the connector); turned on again, the switch applies to new tasks as before;
|
|
96
100
|
- the conversation is rotated or replaced (`/new`, the daily or idle rotation): an earlier conversation never controls the Mac again;
|
|
97
101
|
- SynaBun shuts down;
|
|
98
102
|
- someone stops it at the Mac: `Esc`, the pointer in the top-left corner, or the red *Stop* button. The phone is told in one line ("Computer control was stopped on your Mac: I am no longer controlling it."), and computer use stays latched off until *Resume* is pressed on the computer, as for any other session.
|
|
99
103
|
|
|
100
104
|
**What "stopped" covers.** Every computer action is admitted once and then fenced: it is checked again when its turn in the execution queue comes and before each command it sends to the helper, and it is bound to the turn it was admitted in. When control ends, actions still waiting are answered `CONTROL_ENDED` (or `STOPPED_BY_USER`) and never reach the helper, the helper's command in flight is aborted, and held mouse buttons and keys are released before the desktop is released. What can still complete: the one helper command that was already executing at that instant, when it is past its last abort check (for example a click that was already posted, or an accessibility action after its element reads). No further command follows it.
|
|
101
105
|
|
|
102
|
-
**What does not change.** Every guard of computer use applies to a WhatsApp conversation exactly as on the computer: blocked apps (password managers, terminals, web browsers), protected windows, password fields, the locked screen, the pause while you use the mouse or keyboard, one controller at a time, the rate limit. The Assistant is told to describe what it did
|
|
106
|
+
**What does not change.** Every guard of computer use applies to a WhatsApp conversation exactly as on the computer: blocked apps (password managers, terminals, web browsers), protected windows, password fields, the locked screen, the pause while you use the mouse or keyboard, one controller at a time, the rate limit. The Assistant is told to describe what it did and what is on screen (a screenshot you ask for as a file is sent with `whatsapp_send_file`: [Sending files](#sending-files)), to stop and say so when the screen is locked or asleep, and never to unlock it or type passwords or codes. Agents started from WhatsApp never get computer use, at any level: only the Assistant's own computer tools are covered. The desktop audit log records, for every action of a WhatsApp conversation, that it came from WhatsApp and whether it ran unasked (Autonomous) or under an approved task.
|
|
103
107
|
|
|
104
108
|
**In the Assistant panel.** The *Computer* switch of a WhatsApp conversation shows what is in effect and says why in its tooltip ("Off for WhatsApp. Turn it on in Settings → Messages → WhatsApp → Safety", "Asks on your phone once per task", "On while Autonomous is active", "Off: WhatsApp is paused"). It is not a per-conversation switch there: clicking it, the *Computer use* item of the panel's menu and `/computer` explain instead of toggling.
|
|
105
109
|
|
|
@@ -124,6 +128,8 @@ The approval is bound to the card you saw: SynaBun records on the card, when it
|
|
|
124
128
|
| `/help` | the list |
|
|
125
129
|
| `//text` | send text that starts with a slash |
|
|
126
130
|
|
|
131
|
+
`/status` includes session spend as `Spend: $spent of the $amount budget`, or `Spend: $spent (no limit)`. The session total includes the brain and agents; a metered brain is called out separately. The saved budget is changed in the Assistant UI.
|
|
132
|
+
|
|
127
133
|
Levels, models and settings never change from the phone (`/level`, `/model`, `/config`, `/allow` … get a fixed refusal).
|
|
128
134
|
|
|
129
135
|
## Questions and approvals
|
|
@@ -149,14 +155,14 @@ A tool approval still shows the whole request exactly as it runs, every argument
|
|
|
149
155
|
|
|
150
156
|
**Any message carries the conversation on.** While something waits for you, a message that clearly answers it answers it. Anything else is never told to "answer first" and never waits behind it:
|
|
151
157
|
|
|
152
|
-
- A pending **question** takes your text as its answer, so the Assistant reads it as the reply and carries on in the same turn.
|
|
158
|
+
- A pending **question** takes your typed text as its answer, so the Assistant reads it as the reply and carries on in the same turn. A voice note does not answer it: the question closes without an answer and the transcript goes in as your next message.
|
|
153
159
|
- A pending **approval** (tool, route, plan, a worker's request) is closed **without being granted**: denied or cancelled, never approved. Your message then goes to the Assistant as the next prompt, and it answers that. The brain is told you wrote something else and to end its turn with no text unless it has a result to report; what it still tries to ask in that turn closes the same way, unseen.
|
|
154
160
|
- **The closing text of that turn is never dropped.** Whatever the brain still says when it ends the turn is sent to you as it is, whatever its length, before the answer to your new message: a short result such as "Tests passed: 48/48. Upgrade skipped." arrives. A turn that ends with nothing to say sends nothing (no "Done.").
|
|
155
161
|
- **A request that appears after you already sent a message.** If a message of yours is already on its way when the Assistant raises a question or an approval (it waits for the running turn to end, or it is still being gathered with other quick messages in the 1.5-second window), the request closes at once and is never shown to you: an approval is denied, a question is cancelled, and your message goes in as the next prompt as soon as that turn ends. What you wrote before a request existed is never taken as its answer, not even a `yes`. A question about where a task runs closes the same way, inside its own send: the Assistant hears at once that you wrote something else (it does not wait for a choice that will never come), and a worker that would have waited for that choice is refused instead of being left waiting (`ROUTE_CANCELLED`: nothing stays "waiting for a model choice"). A worker's own request is still shown: it holds no turn, so no message waits behind it.
|
|
156
162
|
- Several things pending at once: all of them close the same way. Nothing can deadlock. Only the current conversation's requests close: a late request from a worker of an earlier conversation (before `/new` or a rotation) stays open, is shown when its turn comes and is still yours to answer.
|
|
157
163
|
- A message sent while the Assistant is working (nothing waiting for you) goes in as soon as that turn ends; messages that wait together go in as one prompt. A turn whose end never arrives is let go after 20 seconds of silence by a queue watch, without you having to write again.
|
|
158
164
|
|
|
159
|
-
**What grants an approval, and what never does.** An approval is granted only by a deterministic match in the bridge (`lib/whatsapp/cards.js`) on a message the owner typed: `yes` (also `y`, `ok`, `okay`, `sure`, `go ahead`, `do it`, `sim`, 👍 and a few more) as the *whole* message, or for a route the name of an offered model. Never by the model, and never by a message that merely contains a yes ("yes but only the tests" approves nothing and becomes the next prompt). A forwarded message, a picture and `//text` are never an answer. When more than one request is open, or right after the desktop answered one, a yes must carry the request's 3-letter code ("yes K7Q"); a no needs none.
|
|
165
|
+
**What grants an approval, and what never does.** An approval is granted only by a deterministic match in the bridge (`lib/whatsapp/cards.js`) on a message the owner typed: `yes` (also `y`, `ok`, `okay`, `sure`, `go ahead`, `do it`, `sim`, 👍 and a few more) as the *whole* message, or for a route the name of an offered model. Never by the model, and never by a message that merely contains a yes ("yes but only the tests" approves nothing and becomes the next prompt). A forwarded message, a picture, a voice note ([its transcript](#voice-notes) is an ordinary message) and `//text` are never an answer. When more than one request is open, or right after the desktop answered one, a yes must carry the request's 3-letter code ("yes K7Q"); a no needs none.
|
|
160
166
|
|
|
161
167
|
**A reply that quotes a message is aimed at that message.** Quoting the request itself answers that request, while it is open, and needs no code. Quoting anything else never answers a request that is open: an earlier request that is closed ("use Opus" quoting an old route question does not pick Opus on a newer one), one of the Assistant's ordinary messages (a `yes` to a question it asked in plain text does not approve a tool or a worker that happens to be waiting), or a message SynaBun does not know. What is open then closes without a grant and your message goes on as the next prompt, with the quoted text as context. A reply that is nothing but an answer (`yes`, `no`, `skip`, a bare option number) and quotes a request which is already closed gets "Already answered in SynaBun." and changes nothing. A reply that says more ("1. Explain the command first", "no, use staging instead") is a new message like any other, whatever it quotes: what is open closes without a grant and it goes on as the next prompt. The `ALLOW 1234` flow for Autonomous and the permission levels are separate from all this and unchanged.
|
|
162
168
|
|
|
@@ -168,6 +174,8 @@ A WhatsApp conversation never sits waiting for workers. The Assistant dispatches
|
|
|
168
174
|
|
|
169
175
|
Two things hold this, besides the persona: `agent_wait` returns after one second for a WhatsApp conversation (`lib/assistant-api.js`), with what is already done and a note telling the brain to end its turn because the result arrives as a mailbox event; desktop sessions wait as they asked. And a message that arrives while a turn runs is delivered when that turn ends, for every brain (Claude, Codex, OpenCode). Adding a message to a turn that is still running is not done: the Claude CLI could take it, but the bridge would have to tell a merged turn from a queued one, and the route gate resets on every prompt.
|
|
170
176
|
|
|
177
|
+
**A task continued on another model answers you here.** When the router moves your task to another model in the same conversation (a route it chose by itself, a remembered route, or a model you named on a card), the Assistant first says so in one line and ends its turn; the turn that does the task on the new model follows, and its answer is the reply to your message, sent once, whether or not *Forward background results* is on. The runtime marks that turn before any of its output (`synabun.continuation`, with where the prompt it continues came from), and it stays a turn SynaBun starts itself, not a new message of yours: what it may do is decided as for any such turn (at Autonomous it asks once on your phone before it uses the Mac; its questions and approvals reach your phone and take your answer). A task you typed into the conversation on the computer keeps its continuation on the computer, as before.
|
|
178
|
+
|
|
171
179
|
## The model
|
|
172
180
|
|
|
173
181
|
Conversation → **Model** decides which brain (provider, model, effort) runs the WhatsApp conversation.
|
|
@@ -189,15 +197,124 @@ A change applies **from your next message**, with nothing else to do: the bridge
|
|
|
189
197
|
| Reply label | `SynaBun:` in Message yourself, none for a second number; up to 24 characters |
|
|
190
198
|
| Long answers | Up to 3 messages · 1 · up to 5 (3,500 characters each; the rest stays in SynaBun) |
|
|
191
199
|
| Fresh conversation | After 6 idle hours · every day at 04:00 · never |
|
|
192
|
-
| In "Message yourself" | Every message · only messages that start with `sb ` / `sb:` (commands, ALLOW and replies to SynaBun always pass) |
|
|
200
|
+
| In "Message yourself" | Every message · only messages that start with `sb ` / `sb:` (commands, ALLOW and replies to SynaBun always pass; a voice note when you say "sb" first: [Voice notes](#voice-notes)) |
|
|
201
|
+
| Transcribe voice notes | On · Off: turned into text on this computer ([Voice notes](#voice-notes)); the line under the switch says whether this computer can, or what is missing |
|
|
193
202
|
|
|
194
203
|
**Open conversation** closes Settings and opens the WhatsApp conversation in the Assistant panel; **Start a fresh conversation** is the desktop's `/new`.
|
|
195
204
|
|
|
196
|
-
Fixed limits: text
|
|
205
|
+
Fixed limits: text, pictures (4 per message, 5 MB each) and voice notes ([transcribed on this computer](#voice-notes), up to 10 minutes and 20 MB each; video, documents and stickers are not read yet), 20 messages a minute and 300 a day. Messages that wait for a running turn are merged into one prompt (up to 12,000 characters and 4 pictures; past that, 5 prompts can wait in line).
|
|
197
206
|
|
|
198
207
|
**While SynaBun is off.** WhatsApp holds what you send while SynaBun (or the computer) is off and delivers it when SynaBun reconnects. Held messages younger than 24 hours pass the same owner checks as live ones; those sent in the last 10 minutes are answered, older ones are skipped with one note ("I was offline and skipped N older messages; resend what you still need."), and skipped messages never count against the limits above. Older than 24 hours, other chats, history sync and SynaBun's own echoes are dropped.
|
|
199
208
|
|
|
200
|
-
**
|
|
209
|
+
**Staying connected.** A linked device stays linked across drops, crashes and restarts of SynaBun: the saved credentials are reused every time, and SynaBun never gives up on its own. A dropped connection is retried after 2, 4, 8, 16, 32, 64, 128 and 256 seconds and then every 5 minutes, each wait ±20% and never above 5 minutes; 10 minutes connected start the ladder over. A connector process that exits is started again after 1, 5, 15 and 60 seconds, then every 5 minutes, and reconnects the link by itself. *Reconnect now* skips the wait. Two kinds of disconnect retry from the longest wait and say so in Settings until the connection has been stable for 10 minutes (until then any other drop waits as long too), with one Activity line per episode: another copy using the same session (`440`, *replaced*), so the two copies do not take the session from each other every few seconds, and an account WhatsApp refuses (`403`, *forbidden*). Only a logout (`401`, removed under Linked devices or too long offline), a session WhatsApp explicitly calls invalid, or a saved session WhatsApp answers with a new QR code needs pairing again (*Link again*). Turning WhatsApp off (`enabled: false`) stops the connector and every retry and ends WhatsApp computer use; turning it on resumes the saved session. A pause does neither: it only stops SynaBun acting on messages. Every disconnect leaves one line in the server log with its code, its reason and what happens next ("recovering automatically", or "link again to pair this device").
|
|
210
|
+
|
|
211
|
+
**Replies that cannot go out yet.** SynaBun sends at most 10 messages a minute, 120 an hour and 500 a day to keep traffic low; status reactions (👀 ✅ ⏳ ❌ ⏹️) stop three short of each cap, so they never hold a reply back. A reply over the cap waits and goes out when the cap frees up; a reply refused while WhatsApp is reconnecting or the connector is restarting waits for the next connection (up to 20). Two refusals drop the reply instead, and Activity says why: sending paused after an owner anomaly (`PAUSED`), and a connector process of another SynaBun version, stopped until SynaBun restarts (`HELD`: an incompatible IPC protocol, after an update installed while SynaBun ran; an ordinary crash is not `HELD`, the connector restarts and the reply waits). A question or an approval that waited this way is still the same request when it arrives: quoting it answers it. One that closed while it waited (answered on the computer, timed out) is not sent late.
|
|
212
|
+
|
|
213
|
+
## Voice notes
|
|
214
|
+
|
|
215
|
+
A voice note, or any audio message, is turned into text **on this computer** and reaches the Assistant like a message you typed, starting with `[Voice note transcript]`. Nothing goes to a transcription service or an API: FFmpeg decodes the note and [whisper.cpp](https://github.com/ggml-org/whisper.cpp)'s `whisper-cli` transcribes it with a model file on this disk. **SynaBun does not ship or download FFmpeg, whisper.cpp or a model**: you install them ([below](#install-whispercpp-ffmpeg-and-a-model)). Until all three are found, the line under *Transcribe voice notes* (Settings → WhatsApp → Conversation) says what is missing and a voice note gets a reply that says why it was not read.
|
|
216
|
+
|
|
217
|
+
**How it works.** The connector process (the only one that loads Baileys) handles a voice note after the owner and duplicate checks, like any message. Before anything is downloaded it checks that transcription is on, that the engine, the model and FFmpeg are found, and that the note's declared size (20 MB) and length (10 minutes) are within the limits. Then:
|
|
218
|
+
|
|
219
|
+
1. The note is downloaded (30 s at most) and written to a scratch folder of its own.
|
|
220
|
+
2. FFmpeg decodes it to 16 kHz mono PCM: `-nostdin -hide_banner -loglevel error -n -protocol_whitelist file,pipe -format_whitelist ogg,mp3,mov,aac,amr,amrnb,amrwb,wav,matroska,flac -codec_whitelist opus,vorbis,mp3float,mp3,aac,aac_fixed,alac,amrnb,amrwb,flac,pcm_s16le,pcm_s16be,pcm_s24le,pcm_s32le,pcm_f32le,pcm_u8,pcm_alaw,pcm_mulaw -i file:<note> -vn -t 601 -ac 1 -ar 16000 -f s16le file:<pcm>`, 30 s at most. It reads local files only, only the containers WhatsApp sends audio in and only audio decoders: a forwarded note is third-party data, so playlists, `concat` lists, other formats and other decoders are refused (checked against FFmpeg 9.0.1: ogg, m4a, mp3, wav, webm, ADTS AAC and flac decode, cover art included; AIFF, a `concat` list and an HLS playlist are refused).
|
|
221
|
+
3. The decoded length is checked again: over 600 s is too long, whatever the message said about itself (a note without duration metadata included). The PCM gets a WAV header.
|
|
222
|
+
4. `whisper-cli -m <model> -f <wav> -l <language> -t <min(8, CPU threads)> -bs 1 -bo 1 -nt -otxt -of <out>` transcribes it, 3 minutes at most. There is no `-ng`: a Metal or CUDA build uses its GPU.
|
|
223
|
+
5. `<out>.txt` becomes one line (whitespace collapsed, whisper.cpp's `[BLANK_AUDIO]` dropped); no words is a failure.
|
|
224
|
+
|
|
225
|
+
Both programs run with `execFile` and `windowsHide`, never through a shell, with a minimal environment (where to look and where temporary files go: no keys, no proxies). A program found on PATH is never one inside SynaBun's own folder, a dependency's `node_modules` or a Windows app alias folder. One note is transcribed at a time; messages you send after a voice note wait until its transcript is ready, so they keep their order. Typed `/stop`, `/pause`, `/status`, `/help` and `/cards` do not wait: they run at once, as commands do (`/resume` and `/new` keep their place, because going ahead of what you sent before them would change what those messages do). What you sent before a `/stop` and arrives after it (the voice note still being transcribed, and what waited behind it) is not acted on, as `/stop` clears what already waits in line; one message says so. What WhatsApp held while SynaBun was offline arrives in order and is not affected. A voice note sent more than 9 minutes ago (held while SynaBun was offline) is not transcribed: the bridge skips messages older than 10 minutes, and the minute between keeps a note from going stale while it is transcribed. Whisper's small model on a recent CPU takes a few seconds for a short note (the reporter measured 4.2 s for 13 s of speech and 7.5 s for 60 s, on a Ryzen 7 5700X without a GPU).
|
|
226
|
+
|
|
227
|
+
**What the Assistant gets.** `[Voice note transcript] <transcript>`, then a line break and the caption when there is one, cut at 8,192 characters with the transcript first. A forwarded voice note stays forwarded: the Assistant reads it as untrusted data, like a forwarded text, and a reply that quotes something keeps its quote. Speech recognition gets words wrong at times ("Amanhã" came out as "Há manhã" in one report): set `transcriptionLanguage` to the language you speak, and read back anything that matters.
|
|
228
|
+
|
|
229
|
+
**A voice note approves nothing.** Its transcript is an ordinary message (`voice: true` on the inbound message), so it is never a command (a spoken "/stop" is a message), never the `ALLOW` code, never the answer to an approval or a route question (a spoken "yes" closes the request without a grant and goes on as your next message) and never the answer to a question. Approvals take a typed `yes`. A voice note you record in WhatsApp is your message like a typed one. An audio file you attach instead (`audioFile: true`: music, a meeting, someone else's recording) is treated like a picture: its words may be anyone's, so its turn runs capped at Ask.
|
|
230
|
+
|
|
231
|
+
**"Only messages that start with sb".** A voice note reaches SynaBun when you say "sb" first (Whisper writes it as "SB", "SB," or "S.B."; the prefix is removed and the marker stays), when its caption starts with `sb`, or when it replies to one of SynaBun's messages. Any other voice note in "Message yourself" is a note to self.
|
|
232
|
+
|
|
233
|
+
**Privacy.** The note, its PCM and WAV copies and the text file live in a private scratch folder (`<folder>/scratch/vn-*`: 0700, files 0600, one per note) that is removed when the note is done, whatever happened; one a crash left behind is removed when the connector starts again. The transcription folder is outside every backup. No output of FFmpeg or whisper-cli is logged, returned or kept (it can hold the transcript): the server log gets one line naming the step that failed. The transcript is message text: the Activity log keeps it only with *Keep message text in the activity log* on. Once transcribed, the text goes to the Assistant's brain like a typed message, and so to that model's provider, as typed text would.
|
|
234
|
+
|
|
235
|
+
**When a note is not transcribed** the phone hears why (once per reason every 10 minutes) and a caption still goes on as a message. The reason is `unsupported.reason` of the inbound message (`{ type: 'audio', reason }`):
|
|
236
|
+
|
|
237
|
+
| Reason | When |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `transcription_disabled` | *Transcribe voice notes* is off. Nothing is downloaded |
|
|
240
|
+
| `transcription_unavailable` | whisper-cli, the model or FFmpeg is not found, or a path set for one does not work. Nothing is downloaded |
|
|
241
|
+
| `too_large` | over 20 MB, as declared (nothing downloaded) or as downloaded |
|
|
242
|
+
| `too_long` | over 10 minutes, as declared (nothing downloaded) or as decoded |
|
|
243
|
+
| `stale` | sent more than 9 minutes ago, while SynaBun was offline. Nothing is downloaded |
|
|
244
|
+
| `transcription_failed` | the download, FFmpeg or whisper-cli failed or timed out, a program or the model was gone when it was about to run, or no words came out |
|
|
245
|
+
|
|
246
|
+
### Install whisper.cpp, FFmpeg and a model
|
|
247
|
+
|
|
248
|
+
The transcription folder is `DATA_HOME/runtime/whatsapp-transcription/` (`~/.synabun/runtime/whatsapp-transcription` by default; `%APPDATA%\synabun\runtime\whatsapp-transcription` on Windows), next to the connector. SynaBun looks for, in this order:
|
|
249
|
+
|
|
250
|
+
- **whisper-cli**: the path in `transcriptionEnginePath`; else `<folder>/bin/whisper-cli` (`whisper-cli.exe` on Windows), `<folder>/bin/Release/whisper-cli.exe` (the layout of the official Windows zip), then `whisper-cli` on PATH and, on macOS, in `/opt/homebrew/bin` and `/usr/local/bin` (a server started from the Dock or Finder may not have them on PATH).
|
|
251
|
+
- **FFmpeg**: the path in `transcriptionFfmpegPath`; else `<folder>/bin/ffmpeg` (`ffmpeg.exe`), then PATH and the same Homebrew folders.
|
|
252
|
+
- **The model**: the path in `transcriptionModelPath`; else `<folder>/ggml-small.bin`.
|
|
253
|
+
|
|
254
|
+
A path set in the settings is used as it is or not at all: one that does not work makes that part missing and is never replaced by a search. Relative PATH entries and network folders are never searched.
|
|
255
|
+
|
|
256
|
+
- **macOS**: `brew install whisper-cpp ffmpeg` (Homebrew's whisper-cpp is built with Metal).
|
|
257
|
+
- **Windows**: unpack the official Windows zip of a [whisper.cpp release](https://github.com/ggml-org/whisper.cpp/releases) into `<folder>\bin`, keeping its `Release` folder with the DLLs beside `whisper-cli.exe`; put `ffmpeg.exe` of an FFmpeg build ([ffmpeg.org](https://ffmpeg.org/download.html) lists them) into `<folder>\bin`, or on PATH.
|
|
258
|
+
- **Linux**: your distribution's `ffmpeg` package, and its whisper.cpp package where it has one (the program must be named `whisper-cli`), or build whisper.cpp (`cmake -B build && cmake --build build --config Release`) and copy `build/bin/whisper-cli` into `<folder>/bin`.
|
|
259
|
+
- **The model**, on every system: download [`ggml-small.bin`](https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin) (multilingual, about 466 MiB) into `<folder>/`. Another ggml model works through `transcriptionModelPath`.
|
|
260
|
+
|
|
261
|
+
Installed while SynaBun runs: the connector and Settings look again within a minute (turning the switch off and on looks at once). Nothing needs a restart.
|
|
262
|
+
|
|
263
|
+
### Settings and API
|
|
264
|
+
|
|
265
|
+
Settings → WhatsApp → Conversation has the switch (*Transcribe voice notes*) and the line under it: "Voice notes are transcribed on this computer.", or "Voice notes: not set up — missing …". The other fields are set with `PUT /api/whatsapp/config`, behind the same guards as the Settings tab (this computer only):
|
|
266
|
+
|
|
267
|
+
| Field | Default | Values |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `transcriptionEnabled` | `true` | `true` · `false` |
|
|
270
|
+
| `transcriptionLanguage` | `'auto'` | `'auto'` (whisper.cpp detects it per note) or a 2-3 letter lowercase code (`en`, `pt`, `yue`) |
|
|
271
|
+
| `transcriptionEnginePath` | `''` | `''` (found by itself) or the absolute path of the whisper-cli program |
|
|
272
|
+
| `transcriptionModelPath` | `''` | `''` (`<folder>/ggml-small.bin`) or the absolute path of a ggml model |
|
|
273
|
+
| `transcriptionFfmpegPath` | `''` | `''` (found by itself) or the absolute path of the ffmpeg program |
|
|
274
|
+
|
|
275
|
+
A path must be the absolute path of an existing regular file on this computer (links are followed), a program executable (an `.exe` on Windows), the model readable and not empty; it is never a name looked up on PATH. Anything else is `400 CONFIG_INVALID` with `field`. The connector checks the file again right before it runs it. Only the person at this computer sets a path: a service write may clear one but never set it, no phone command touches them, and remote sessions are refused the transcription folder (`lib/remote-policy.js`), so nothing but you chooses what the connector runs. A `PUT` that changes any of these fields re-configures the connector at once. `GET /status` reports `transcription { enabled, available, missing, invalid, language, home }` (`missing` / `invalid`: `engine`, `model`, `ffmpeg`).
|
|
276
|
+
|
|
277
|
+
## Sending files
|
|
278
|
+
|
|
279
|
+
Ask for a file and the Assistant sends it to this chat as a real WhatsApp attachment: a document, a photo, a video or an audio file, including images and videos it had a worker generate. It uses the assistant-only MCP tool `whatsapp_send_file({ path, caption?, as? })`, and only when you asked for a file. For generated media it takes the local path from the worker's result (SynaBun keeps those copies in `DATA_HOME/data/media/`). There is no recipient and no session parameter: the file goes to the bound owner, in the WhatsApp conversation whose brain called the tool, and to nobody else.
|
|
280
|
+
|
|
281
|
+
**Forms.** `as` is `auto` (the default), `document`, `image`, `video` or `audio`:
|
|
282
|
+
|
|
283
|
+
| Extension | `auto` sends it as | MIME type |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `.jpg` `.jpeg` · `.png` · `.webp` | a photo | `image/jpeg` · `image/png` · `image/webp` |
|
|
286
|
+
| `.mp4` | a video | `video/mp4` |
|
|
287
|
+
| `.ogg` `.opus` · `.mp3` · `.m4a` | audio (an audio file, not a voice note) | `audio/ogg` · `audio/mpeg` · `audio/mp4` |
|
|
288
|
+
| `.pdf` `.md` `.txt` `.csv` `.json` `.patch` `.diff` `.zip` `.7z` `.gz` `.doc` `.docx` `.xls` `.xlsx` `.pptx` `.gif` `.svg` `.wav` `.webm`, anything else | a document, with its name | its type (`application/pdf`, `text/markdown`, …), else `application/octet-stream` |
|
|
289
|
+
|
|
290
|
+
`document` sends the original file untouched, whatever it is. A native form that does not match the extension (`as: 'image'` for a PDF) is refused: use `auto` or `document`. Photos and videos go without a preview thumbnail: SynaBun never decodes the file it sends (building a video preview would mean running FFmpeg).
|
|
291
|
+
|
|
292
|
+
**Limits.** A photo, video or audio file over 16 MiB goes as a document instead (`fallback: true` in the answer); nothing over 100 MiB is sent, and that is refused before the file is read. Native audio has no caption: a caption with it is refused, so leave it out or send the file as a document. A caption holds up to 1,024 characters, a path up to 2,048. A file counts against the send limits like a reply (10 a minute, 120 an hour, 500 a day).
|
|
293
|
+
|
|
294
|
+
**What is refused** (`FILE_FORBIDDEN`), on the path as written **and** on the real file it leads to (links are followed and checked too), on macOS, Linux and Windows:
|
|
295
|
+
|
|
296
|
+
- the WhatsApp session itself (`DATA_HOME/whatsapp/auth`, wherever `SYNABUN_WHATSAPP_HOME` puts it) and SynaBun's data home (`DATA_HOME` and `~/.synabun`: `data/`, `mcp-data/`, `runtime/`, backups, `.env`, the memory database, the Claude and Codex account folders), except `data/media/`, where the generated images and videos and saved browser screenshots are;
|
|
297
|
+
- browser profiles: Chrome, Chromium, Brave, Edge (`Microsoft Edge` on macOS too), Firefox profiles on every system (`logins.json` and `key4.db` are its saved passwords), Safari, Arc, Opera, Vivaldi and anti-detect profiles (MoreLogin), plus their password, cookie and key files by name wherever they are;
|
|
298
|
+
- app sign-in stores: Discord, Slack, Telegram Desktop (`tdata`), Signal, WhatsApp Desktop;
|
|
299
|
+
- password managers (1Password, Bitwarden, KeePass `*.kdbx`), `~/Library/Keychains`, GNOME Keyring and KWallet, Windows Credential Manager (with its DPAPI keys and the Vault), the Mail and Messages stores and Thunderbird;
|
|
300
|
+
- shell histories (`.zsh_history`, `.bash_history`, fish's history, PowerShell's), `.netrc`, `.npmrc`, `.pypirc`, `.pgpass`, `.git-credentials` and git's `~/.config/git/credentials`, `.vault-token`, the `.docker`, `.kube`, `.aws`, `.azure`, gcloud, `gh`, `.gnupg` and `.ssh` folders, and CLI token stores (cargo, gem, Pulumi, Hugging Face, Kaggle, rclone, hub, glab, doctl, Stripe, Fly, cloudflared, OCI, Databricks);
|
|
301
|
+
- key and certificate files anywhere (`*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `*.ppk`, `*.p8`, SSH `id_*` keys), `.env` files and their variants, terraform state;
|
|
302
|
+
- AI tool configuration and credentials: `~/.claude.json`, `~/.claude*`, `~/.codex*`, OpenCode's configuration and `auth.json`, GitHub Copilot's tokens, `.cursor/mcp.json`, `claude_desktop_config.json`, `mcp-api-key` and `.synabun-api-key` files;
|
|
303
|
+
- copies of those places' secret parts wherever they are (a backup disk, Time Machine): `.synabun/whatsapp`, `mcp-data`, `runtime` and `backups`, `sidepanel-configs`, `.claude/settings*.json` and `.claude/projects`, `.codex*/config.toml`, `sessions` and `history.jsonl`; and SynaBun's own backup archives (`synabun-<kind>-<timestamp>.zip`, `synabun-auto-backup.zip`) by their name, in whatever folder the backups go to.
|
|
304
|
+
|
|
305
|
+
The list is explicit (`neural-interface/lib/whatsapp/local-file.js`, `DENYLIST`) and matched without regard to letter case. The protected folders (the session, the data home, `~/.claude*`, `~/.codex*`) are also recognized by what they are on disk, their device and inode, so another spelling of the same folder is refused like the usual one: macOS reaches every folder of the data volume under `/System/Volumes/Data/…` too, which `realpath` keeps. On Windows a name is also checked as Windows opens it, without trailing dots and spaces (`.ssh.\id_rsa` is `.ssh\id_rsa`). **The content of a file is never inspected**: a secret inside an ordinary file with an ordinary name is sent like any other file, so ask only for what you mean to receive. Keynote presentations share the `.key` extension with private keys and are refused too: export them to PDF first.
|
|
306
|
+
|
|
307
|
+
Also refused: a URL, a network share or device path (`\\server\share`, `//server`, `\\?\…`), the system's own namespaces (`/dev`, where `/dev/fd/3` would be the connector's own open file, `/proc`, `/sys`, and macOS's `/.vol`, `/.nofollow`, `/.resolve`, which open any file under another name), a link or a mapped drive that leads to a share or to one of them, and, on Windows, an alternate data stream (`file.txt:stream`), all `FILE_FORBIDDEN`; a relative path or one with a control character (`BAD_REQUEST`); a folder, a missing or unreadable file, an empty file, anything but a regular file (a FIFO, a device) and a file that changes while it is read (`FILE_INVALID`); a file with more than one hard link (`FILE_FORBIDDEN`: its other names cannot be checked against the list).
|
|
308
|
+
|
|
309
|
+
**How it is read.** In the connector process, in the file's turn of the send queue, right before it goes out: the request is checked, the path resolved, both checked against the list and the protected folders by device and inode, the file opened without following a link (`O_NOFOLLOW` where the system has it, `O_NONBLOCK` so a FIFO swapped in cannot hang it) and compared with what the checks saw (device, inode, size; its real path unchanged and still naming that very file, so a parent folder swapped for a link and back is caught; on Linux the open file's own name is checked against the list too), read to its end and not one byte further, and checked again when the read ends. The owner the request was accepted for must still be the owner before the read (`NO_OWNER`), and the connection must not change while the file is read (`NOT_CONNECTED`): a disconnect, another owner or a pause while the file is read sends nothing. A reconnect while the file waits its turn is no change of owner: like a text reply, it goes on the new link.
|
|
310
|
+
|
|
311
|
+
**When it is refused.** The answer names the file and the reason (`…/.ssh/id_ed25519 is protected (credentials (SSH, GnuPG, cloud, registry and git logins)): SynaBun never sends it from WhatsApp`), so the Assistant can tell you why; numbers, JIDs and credentials in the path are redacted.
|
|
312
|
+
|
|
313
|
+
**Who may send.** Only the active WhatsApp conversation's own Assistant brain, at any level (Read-only included) and on any brain (Claude, Codex or OpenCode). Delivering a file you asked for is a reply, like the final text: it needs no route of its own, is never asked on the phone at Ask, and works in plan mode and on the read-only Codex and OpenCode brains. It grants nothing else: no file edits, no computer use, no worker permissions. The brain proves who it is with a credential SynaBun makes for each brain launch of a WhatsApp conversation and gives only to that brain's SynaBun MCP server, through the environment of the brain's own process, never a command line or a file: the Claude CLI expands it into its MCP header, Codex forwards it to its MCP child by name, OpenCode's serve fills it into its MCP entry. The `X-Synabun-Role` and `X-Synabun-Terminal` headers, which any local program can send, prove nothing here. A worker, a desktop chat and an earlier WhatsApp conversation are refused (`403`), and so is any request while WhatsApp is off, paused or not connected (`409`). See [SECURITY.md](../SECURITY.md).
|
|
314
|
+
|
|
315
|
+
**When the answer is uncertain.** The send is exactly one request, never retried by SynaBun or by the Assistant. A success (`{ ok, id, type, mimetype, size, fileName, path, fallback }`, `path` the canonical one) means WhatsApp took the file. A timeout (the connector gives a file 120 seconds, the tool 130; each brain's MCP client waits longer: Codex and OpenCode 180 seconds), a connector that stops during the send, or an answer lost on the way back (a reset connection) answers with `uncertain: true`: the file may or may not have arrived, and the Assistant says so instead of sending it again. Ask again if it did not arrive. `UNREACHABLE` (nothing listening for the request) means nothing was sent.
|
|
316
|
+
|
|
317
|
+
**What is kept.** Every real send attempt leaves one line in the server log with the canonical path, the size, the form and the masked chat (`[whatsapp] file send {"path":…,"size":…,"chat":"••••1234","type":"document"}`), never the bytes or the caption; numbers and JIDs in the path are redacted. Activity records each outcome with the path and the size, or the refusal's code; for a refusal the path is the Assistant's own text, so long key-like runs in it are masked too (`[key]`).
|
|
201
318
|
|
|
202
319
|
## Security model
|
|
203
320
|
|
|
@@ -205,6 +322,8 @@ Fixed limits: text and pictures (4 per message, 5 MB each; voice, video, documen
|
|
|
205
322
|
|
|
206
323
|
**Owner binding.** Only the owner is answered: in Message yourself, your own chat after you confirmed the account; in second-number mode, the account that sent the claim code. Everything else is dropped by the host and only counted (*Ignored* in Activity).
|
|
207
324
|
|
|
325
|
+
**Sending files.** `POST /api/assistant/whatsapp/send-file` sits outside `/api/whatsapp` and has its own guards: no invite guests, no tunnel or proxy headers, a loopback socket, a localhost `Host` on SynaBun's port, a JSON body of `path`, `caption` and `as` only, and the calling brain's credential (`X-Synabun-Brain-Token`), compared in constant time ([Sending files](#sending-files)).
|
|
326
|
+
|
|
208
327
|
**The HTTP API** (`/api/whatsapp`, `lib/http-guards.js`):
|
|
209
328
|
|
|
210
329
|
- every route: no invite guests (`GUEST_FORBIDDEN`), no tunnel or proxy headers (`cf-connecting-ip`, `cf-ray`, `x-forwarded-*`, `forwarded`, `x-real-ip`, `via` → `REMOTE_FORBIDDEN`), a loopback socket only (`LOCAL_ONLY`, read from `req.socket.remoteAddress`, never `X-Forwarded-For`), a `Host` of `localhost` / `127.0.0.1` / `[::1]` on SynaBun's port (`BAD_HOST`, DNS rebinding), `Cache-Control: no-store`;
|
|
@@ -221,6 +340,7 @@ App-wide, every WebSocket upgrade (`/ws/*`) must carry no `Origin` or this serve
|
|
|
221
340
|
|---|---|
|
|
222
341
|
| WhatsApp session keys | `DATA_HOME/whatsapp/auth/` (`%LOCALAPPDATA%\synabun\whatsapp` on Windows; `SYNABUN_WHATSAPP_HOME` overrides): outside every folder a backup copies (`data/`, `mcp-data/`, the skills / agents / skins roots), so backups never copy a live session. On macOS and Linux the folder must be yours at 0700 and its files at 0600, or the store does not open (`AUTH_PERMS`) |
|
|
223
342
|
| The connector | `DATA_HOME/runtime/whatsapp/` |
|
|
343
|
+
| Voice notes | `DATA_HOME/runtime/whatsapp-transcription/`: whisper.cpp, FFmpeg and the model you put there, and `scratch/`, which holds a note only while it is transcribed |
|
|
224
344
|
| Activity log | `DATA_HOME/data/logs/whatsapp-YYYYMMDD.log`, mode 0600, deleted after 7 days |
|
|
225
345
|
| Settings | kv_config row `whatsapp_config` in the SynaBun database |
|
|
226
346
|
| The conversation | an ordinary Assistant session ("WhatsApp · Sep 28") |
|
|
@@ -233,7 +353,7 @@ App-wide, every WebSocket upgrade (`/ws/*`) must carry no `Origin` or this serve
|
|
|
233
353
|
|
|
234
354
|
## Unlink and remove
|
|
235
355
|
|
|
236
|
-
- **Unlink** logs SynaBun out of WhatsApp (the device disappears from Linked devices), deletes the session keys and resets the conversation pointer, owner, pause and autonomous
|
|
356
|
+
- **Unlink** logs SynaBun out of WhatsApp (the device disappears from Linked devices), deletes the session keys and resets the conversation pointer, owner, pause and autonomous mode (Autonomous goes back to Ask).
|
|
237
357
|
- **Remove WhatsApp from SynaBun** also removes the connector and the activity log and resets every WhatsApp setting; tick *Also delete the WhatsApp conversation* to delete the Assistant conversation too.
|
|
238
358
|
- **Lost your phone?** Press **Pause now**, or on any device open WhatsApp → Settings → Linked devices → SynaBun → **Log out**.
|
|
239
359
|
- Uninstalling SynaBun does **not** unlink WhatsApp: unlink first, or log it out from the phone.
|
|
@@ -248,8 +368,11 @@ App-wide, every WebSocket upgrade (`/ws/*`) must carry no `Origin` or this serve
|
|
|
248
368
|
| Messages from WhatsApp Web / Desktop get no answer | In Message yourself only messages typed on the phone count. |
|
|
249
369
|
| "Logged out from the phone" | Removed under Linked devices, or the phone was offline for 14 days (WhatsApp then logs out every linked device). *Link again*. |
|
|
250
370
|
| "WhatsApp refused this account" | WhatsApp blocked linking for that account; check the WhatsApp app. |
|
|
251
|
-
|
|
|
252
|
-
|
|
|
371
|
+
| "The saved WhatsApp session is invalid" | WhatsApp rejected this device's session (`badSession`), or answered the saved session with a new QR code. Automatic retries cannot repair the credentials: *Link again* to pair the device. |
|
|
372
|
+
| Stuck on "Reconnecting" | SynaBun retries indefinitely with exponential backoff and jitter, capped at 5 minutes, also after connector crashes; *Reconnect now* skips the wait. Saved credentials are reused after a restart. Only a logout (401), an explicitly invalid session, or a rejected saved session needs pairing again. Disabling WhatsApp (`enabled: false`) stops the recovery. Check the computer's internet if it never comes back. |
|
|
373
|
+
| "Another copy is using this WhatsApp session" (while reconnecting) | Another SynaBun, or another program with a copy of the same session, connected with it (`440`). SynaBun retries every 5 minutes so the two copies do not keep taking the session from each other; close the other copy, or unlink and link again. The note goes once the connection has been stable for 10 minutes. |
|
|
374
|
+
| "WhatsApp refused this account; it may be restricted or banned" (while reconnecting) | WhatsApp refuses the account (`403`). The link stays and SynaBun retries every 5 minutes; check WhatsApp on the phone. The note goes once the connection has been stable for 10 minutes. |
|
|
375
|
+
| "The WhatsApp connector does not match this SynaBun version" (`HOST_HELD`) | The connector process speaks another IPC protocol than the running server, after an update installed while SynaBun ran. Restart SynaBun. A connector that merely crashes is restarted by itself and never shows this. |
|
|
253
376
|
| Install failed | The message names the cause (`NPM_NOT_FOUND`, `OFFLINE`, `REGISTRY_REFUSED`, `DISK_FULL`, `NO_PERMISSION`, `FILE_IN_USE`, `TIMEOUT`, `VERIFY_FAILED`); *Try again*. |
|
|
254
377
|
| "SynaBun could not make the WhatsApp session files private" (`AUTH_PERMS`) | The session folder or its files are not yours, or cannot be set to 700 / 600 (a shared or network drive, another user's files). Fix the permissions of the folder shown under Data & privacy, then *Reconnect*. |
|
|
255
378
|
| Replies say the conversation "runs read-only because its brain is Codex / OpenCode" | Ask and Autonomous need a Claude brain: pick a Claude model under Conversation → Model. It applies from your next message. |
|
|
@@ -258,6 +381,14 @@ App-wide, every WebSocket upgrade (`/ws/*`) must carry no `Origin` or this serve
|
|
|
258
381
|
| I replied `yes` to an older message and the approval was denied | A reply that quotes a message answers only that message. Quote the request itself, or send `yes` without quoting anything. |
|
|
259
382
|
| The Assistant never asked, but says it could not run something | You had already sent another message when it wanted to ask, so the request was closed without a grant and your message went first. Ask again when you want it. |
|
|
260
383
|
| The tab says WhatsApp is turned off | `SYNABUN_WHATSAPP=off` is set in SynaBun's environment. |
|
|
384
|
+
| Voice notes get "I can't transcribe voice notes yet" | This computer lacks whisper.cpp, its model or FFmpeg; the line under *Transcribe voice notes* says which. Install them ([Voice notes](#install-whispercpp-ffmpeg-and-a-model)); SynaBun notices within a minute. A path set in the settings that stopped working counts as missing. |
|
|
385
|
+
| A voice note "was not transcribed" | Over 10 minutes or 20 MB, or FFmpeg / whisper-cli failed (the server log names the step, never the words). Try a shorter note, or type it. |
|
|
386
|
+
| I said "yes" in a voice note and the approval was denied | By design: a voice note is an ordinary message and approves nothing. Type `yes`. |
|
|
387
|
+
| Voice notes in "Message yourself" get no answer | With *Only messages that start with sb*, say "sb" first, give the note an `sb` caption, or reply to one of SynaBun's messages. |
|
|
388
|
+
| "…is protected: SynaBun never sends it from WhatsApp" (`FILE_FORBIDDEN`) | The file is in a place SynaBun never sends from, or links to one ([Sending files](#sending-files)). Copy what you want to share into an ordinary folder, or send it from the computer. |
|
|
389
|
+
| "The file may or may not have arrived" | The send timed out or the connector stopped during it. SynaBun does not send it again by itself; check the chat and ask again if it is missing. |
|
|
390
|
+
| "SynaBun is not reachable: nothing was sent" (`UNREACHABLE`) | The brain's MCP server could not reach SynaBun at all, so the file was not sent. Check that SynaBun is running, then ask again. |
|
|
391
|
+
| Transcripts get words wrong | Set `transcriptionLanguage` to the language you speak (`auto` guesses per note), or point `transcriptionModelPath` at a larger ggml model. |
|
|
261
392
|
|
|
262
393
|
**Not the same as the WhatsApp browser tools.** The `whatsapp` MCP profile (`browser_extract_wa_chats`, `browser_extract_wa_messages`) lets agents read WhatsApp Web in SynaBun's browser. The WhatsApp Link is you chatting with SynaBun from your phone. They share nothing.
|
|
263
394
|
|
|
@@ -272,11 +403,13 @@ App-wide, every WebSocket upgrade (`/ws/*`) must carry no `Origin` or this serve
|
|
|
272
403
|
|
|
273
404
|
## API reference
|
|
274
405
|
|
|
406
|
+
`POST /api/assistant/whatsapp/send-file` is the `whatsapp_send_file` tool's route (see [Sending files](#sending-files)).
|
|
407
|
+
|
|
275
408
|
Base path `/api/whatsapp`. Errors are `{ ok: false, code, error, field? }` with the status shown; `429` adds `retryAfterMs` and `Retry-After`, `409 VERSION_CONFLICT` adds `version`.
|
|
276
409
|
|
|
277
410
|
| Route | Body | Answer |
|
|
278
411
|
|---|---|---|
|
|
279
|
-
| `GET /status` | — | The state machine and masked data: `state` (`unavailable` +`reason` (`env`: `SYNABUN_WHATSAPP=off`; `starting`: the server is still starting the Assistant; `error`: the WhatsApp Link failed to load), `not_installed`, `installing` +`stage`, `install_failed` +`code`, `ready`, `linking` +`method` `phase` (`starting`/`waiting`/`scanned`), `link_expired`, `confirm_owner` +`kind` (`self`/`code`), `connected`, `reconnecting`, `paused`, `logged_out` +`reason` (`removed`/`inactive`/`banned`), `error` +`code`), `mode`, `level`, `levelExpiresAt
|
|
412
|
+
| `GET /status` | — | The state machine and masked data: `state` (`unavailable` +`reason` (`env`: `SYNABUN_WHATSAPP=off`; `starting`: the server is still starting the Assistant; `error`: the WhatsApp Link failed to load), `not_installed`, `installing` +`stage`, `install_failed` +`code`, `ready`, `linking` +`method` `phase` (`starting`/`waiting`/`scanned`), `link_expired`, `confirm_owner` +`kind` (`self`/`code`), `connected`, `reconnecting`, `paused`, `logged_out` +`reason` (`removed`/`inactive`/`banned`/`invalid_session`), `error` +`code`), `warning` (`replaced` (440) / `forbidden` (403) while the link retries one of them, until 10 minutes connected; else `null`), `mode`, `level`, `levelExpiresAt` (always `null`: Autonomous has no time limit; kept for older readers), `paused`, `pausedBy`, `computerUse` (the [Computer use](#computer-use) switch), `connector {installed, version, pinned, outdated, stage, errorCode}`, `account {masked, name}`, `owner {masked, boundAt}`, `confirm {kind, expiresAt, attemptsLeft}`, `session {id, title}`, `brainLimit {provider, label, text}` (the conversation's brain makes it read-only), `brainChoice {choice, source: 'assistant'\|'choice', effective {provider, providerLabel, model, modelLabel, effort}, fallback {reason: 'disabled'\|'unknown', provider, model}, readOnly {provider, label}, checked}` (the model selector), `connection {attempt, nextRetryAt}` (the next reconnect, or the next connector start while it is down), `host {state, restarts, retryInMs}`, `transcription {enabled, available, missing, invalid, language, home}` ([Voice notes](#voice-notes)), `counters`, `config`, `version`, `escalation`, `manualCommand`, `paths`. The `whatsapp:status` broadcast carries states, counters, `warning`, `brain` (the stored choice: ids only) and `transcription {enabled, available}` |
|
|
280
413
|
| `POST /setup` | `{ mode: 'self'\|'dedicated', method: 'qr'\|'code', phone? }` | Records the intent (never the number), installs if needed. `{ ok, next: 'install'\|'link'\|'none', status }` — the tab then opens `/link` |
|
|
281
414
|
| `POST /connector/install` | `{ update?, reinstall? }` | Starts an install (`409 INSTALL_BUSY`); progress in `/status` |
|
|
282
415
|
| `DELETE /connector/install` | `{}` | Cancels a running install |
|
|
@@ -290,7 +423,7 @@ Base path `/api/whatsapp`. Errors are `{ ok: false, code, error, field? }` with
|
|
|
290
423
|
| `POST /pause` | `{}` | Pause (agents allowed: JSON body required, no Origin/UI header) |
|
|
291
424
|
| `POST /resume`, `/reconnect`, `/unlink` | `{}` | |
|
|
292
425
|
| `POST /remove` | `{ deleteConversation? }` | Unlink, uninstall, clear the log and every setting |
|
|
293
|
-
| `GET /config` · `PUT /config` | `{ config: {…}, expectedVersion, confirmEscalation? }` | User fields: `enabled`, `level`, `progress`, `forwardBackground`, `replyLabel`, `maxMessages`, `rotation`, `selfTrigger`, `activityText`, `strictWorkerApprovals`, `computerUse` (a boolean, off by default: [Computer use](#computer-use)), `brain` (`null` = same as the Assistant, or `{ provider, model, effort }`: checked against the Assistant's catalog, `400 CONFIG_INVALID {field:'brain'}` for an unknown or disabled model or an effort it does not run, `503 CATALOG_UNAVAILABLE` when it cannot be checked). `400 CONFIG_INVALID {field}` (service-owned fields included), `409 VERSION_CONFLICT`, `400 CONFIRM_REQUIRED`, `409 PHONE_REQUIRED`; raising to Autonomous answers `pending: { code, expiresAt, attemptsLeft }` |
|
|
426
|
+
| `GET /config` · `PUT /config` | `{ config: {…}, expectedVersion, confirmEscalation? }` | User fields: `enabled`, `level`, `progress`, `forwardBackground`, `replyLabel`, `maxMessages`, `rotation`, `selfTrigger`, `activityText`, `strictWorkerApprovals`, `computerUse` (a boolean, off by default: [Computer use](#computer-use)), `brain` (`null` = same as the Assistant, or `{ provider, model, effort }`: checked against the Assistant's catalog, `400 CONFIG_INVALID {field:'brain'}` for an unknown or disabled model or an effort it does not run, `503 CATALOG_UNAVAILABLE` when it cannot be checked), `transcriptionEnabled`, `transcriptionLanguage`, `transcriptionEnginePath`, `transcriptionModelPath`, `transcriptionFfmpegPath` ([Voice notes](#settings-and-api): a path is checked on disk, a change re-configures the connector at once). `400 CONFIG_INVALID {field}` (service-owned fields included), `409 VERSION_CONFLICT`, `400 CONFIRM_REQUIRED`, `409 PHONE_REQUIRED`; raising to Autonomous answers `pending: { code, expiresAt, attemptsLeft }` |
|
|
294
427
|
| `GET /activity` · `DELETE /activity` | `?limit=` | The latest rows (`{at, kind, detail, text?}`) and counters; clear |
|
|
295
428
|
| `POST /session/new` | `{}` | A fresh WhatsApp conversation |
|
|
296
429
|
| `POST /__fake` | `{ action, … }` | Only with `SYNABUN_WHATSAPP_FAKE=1` (404 otherwise) |
|
|
@@ -305,10 +438,12 @@ Base path `/api/whatsapp`. Errors are `{ ok: false, code, error, field? }` with
|
|
|
305
438
|
| `inbound` | `{ from: 'owner'\|'stranger', text, forwarded?, offline?, ageMs? }` — a message from you (your own chat in self mode) or from a stranger (ignored); `offline: true` delivers it the way WhatsApp hands over what it held while SynaBun was off, sent `ageMs` ago |
|
|
306
439
|
| `drop` | the connection drops (reconnect) |
|
|
307
440
|
| `logout` | the phone logs SynaBun out |
|
|
308
|
-
| `ban` | WhatsApp refuses the account |
|
|
309
|
-
| `crash` | the connector process crashes (
|
|
441
|
+
| `ban` | WhatsApp refuses the account (`403`: the link retries every 5 minutes and Settings shows the warning) |
|
|
442
|
+
| `crash` | the connector process crashes (it is started again and reconnects, however often it crashes) |
|
|
310
443
|
| `state` | nothing; every answer includes `sent` (what SynaBun sent, without addresses) and `status` |
|
|
311
444
|
|
|
445
|
+
The `inbound` action sends text. Voice notes are played in process: `fake.inbound({ audio: { data, seconds, fileLength, fail, hang } })` of `fake-baileys.js`, with a stand-in transcriber in the host (`hostOptions.transcriber` of the inproc manager).
|
|
446
|
+
|
|
312
447
|
Tests use fakes throughout, a temporary data home and `SYNABUN_TYPESAFE=off`; they are not published with this repository.
|
|
313
448
|
|
|
314
449
|
## Integration (server.js)
|
|
@@ -1180,9 +1180,11 @@ const LOOP_STALE_INACTIVITY_MS = 45 * 60 * 1000; // 45 min — browser iteration
|
|
|
1180
1180
|
* Scan loop directory and delete stale/inactive loop files.
|
|
1181
1181
|
* No history keeping — ended loops are removed immediately.
|
|
1182
1182
|
*
|
|
1183
|
-
* A loop is stale if
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1183
|
+
* A loop is stale if it is active with no iteration activity for 45 minutes
|
|
1184
|
+
* (session died). A dispatched worker's file (runMode 'task') is never swept
|
|
1185
|
+
* here: its one iteration lasts the whole run, which idles warm between turns
|
|
1186
|
+
* for far longer, and the server ends it (work-time cap, idle timeout, and
|
|
1187
|
+
* marks it inactive after a restart).
|
|
1186
1188
|
*
|
|
1187
1189
|
* @param {string} loopDir - path to data/loop/
|
|
1188
1190
|
* @returns {{ deactivated: string[], deleted: string[] }}
|
|
@@ -1201,6 +1203,7 @@ export function cleanupStaleLoops(loopDir) {
|
|
|
1201
1203
|
const loop = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
1202
1204
|
|
|
1203
1205
|
if (loop.active) {
|
|
1206
|
+
if (loop.runMode === 'task') continue;
|
|
1204
1207
|
const lastActivity = new Date(loop.lastIterationAt || loop.startedAt || 0).getTime();
|
|
1205
1208
|
const noRecentActivity = (now - lastActivity) >= LOOP_STALE_INACTIVITY_MS;
|
|
1206
1209
|
|