synabun 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +205 -662
- package/LICENSE-COMMERCIAL.md +33 -0
- package/NOTICE +17 -0
- package/README.md +175 -853
- package/THIRD-PARTY-LICENSES.md +136 -0
- package/benchmarks/browser-live-results.json +79 -0
- package/benchmarks/browser-v2-results.json +258 -0
- package/benchmarks/fixtures/browser/README.md +57 -0
- package/benchmarks/fixtures/browser/page-state.json +132 -0
- package/benchmarks/fixtures/browser/social.json +92 -0
- package/benchmarks/fixtures/browser/target.json +319 -0
- package/benchmarks/fixtures/desktop/README.md +58 -0
- package/benchmarks/fixtures/desktop/abstention.json +59 -0
- package/benchmarks/fixtures/desktop/adversarial.json +22 -0
- package/benchmarks/fixtures/desktop/forbidden.json +93 -0
- package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
- package/benchmarks/fixtures/desktop/target.json +136 -0
- package/bin/synabun.js +30 -0
- package/docs/whatsapp.md +325 -0
- package/hooks/claude-code/post-plan.mjs +613 -391
- package/hooks/claude-code/post-remember.mjs +246 -122
- package/hooks/claude-code/pre-compact.mjs +171 -110
- package/hooks/claude-code/pre-task.mjs +133 -0
- package/hooks/claude-code/pre-websearch.mjs +15 -4
- package/hooks/claude-code/prompt-origin.mjs +94 -0
- package/hooks/claude-code/prompt-submit.mjs +1244 -861
- package/hooks/claude-code/session-start.mjs +114 -156
- package/hooks/claude-code/shared.mjs +1228 -368
- package/hooks/claude-code/state.mjs +167 -0
- package/hooks/claude-code/stop.mjs +515 -140
- package/hooks/claude-code/subagent-start.mjs +154 -0
- package/hooks/claude-code/subagent-stop.mjs +102 -0
- package/hooks/claude-code/task-gate.mjs +150 -0
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +137 -0
- package/lib/claude-usage-audit.js +141 -0
- package/lib/client-config-repair.js +145 -0
- package/lib/data-home-migration.js +502 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +245 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/paths.d.ts +21 -0
- package/lib/paths.js +103 -0
- package/lib/start-launcher.js +1309 -0
- package/mcp-server/dist/config.d.ts +22 -0
- package/mcp-server/dist/config.d.ts.map +1 -0
- package/mcp-server/dist/config.js +109 -0
- package/mcp-server/dist/config.js.map +1 -0
- package/mcp-server/dist/http.d.ts +30 -0
- package/mcp-server/dist/http.d.ts.map +1 -0
- package/mcp-server/dist/http.js +276 -0
- package/mcp-server/dist/http.js.map +1 -0
- package/mcp-server/dist/index.d.ts +48 -0
- package/mcp-server/dist/index.d.ts.map +1 -0
- package/mcp-server/dist/index.js +538 -0
- package/mcp-server/dist/index.js.map +1 -0
- package/mcp-server/dist/preload-http.d.ts +3 -0
- package/mcp-server/dist/preload-http.d.ts.map +1 -0
- package/mcp-server/dist/preload-http.js +28 -0
- package/mcp-server/dist/preload-http.js.map +1 -0
- package/mcp-server/dist/preload.d.ts +3 -0
- package/mcp-server/dist/preload.d.ts.map +1 -0
- package/mcp-server/dist/preload.js +32 -0
- package/mcp-server/dist/preload.js.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
- package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
- package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
- package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
- package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/assistant-judgments.js +128 -0
- package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
- package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist-gate.js +344 -0
- package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/browser-assist.d.ts +104 -0
- package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-assist.js +282 -0
- package/mcp-server/dist/services/browser-assist.js.map +1 -0
- package/mcp-server/dist/services/browser-bench.d.ts +772 -0
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-bench.js +424 -0
- package/mcp-server/dist/services/browser-bench.js.map +1 -0
- package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
- package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-judgments.js +363 -0
- package/mcp-server/dist/services/browser-judgments.js.map +1 -0
- package/mcp-server/dist/services/browser-risk.d.ts +166 -0
- package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/browser-risk.js +458 -0
- package/mcp-server/dist/services/browser-risk.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +61 -0
- package/mcp-server/dist/services/categories.d.ts.map +1 -0
- package/mcp-server/dist/services/categories.js +377 -0
- package/mcp-server/dist/services/categories.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.js +346 -0
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
- package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
- package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
- package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
- package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-assist.js +266 -0
- package/mcp-server/dist/services/desktop-assist.js.map +1 -0
- package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-bench.js +311 -0
- package/mcp-server/dist/services/desktop-bench.js.map +1 -0
- package/mcp-server/dist/services/desktop-client.d.ts +23 -0
- package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-client.js +79 -0
- package/mcp-server/dist/services/desktop-client.js.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
- package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-judgments.js +177 -0
- package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
- package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
- package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
- package/mcp-server/dist/services/desktop-risk.js +605 -0
- package/mcp-server/dist/services/desktop-risk.js.map +1 -0
- package/mcp-server/dist/services/discord.d.ts +112 -0
- package/mcp-server/dist/services/discord.d.ts.map +1 -0
- package/mcp-server/dist/services/discord.js +477 -0
- package/mcp-server/dist/services/discord.js.map +1 -0
- package/mcp-server/dist/services/edit-stale.d.ts +81 -0
- package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
- package/mcp-server/dist/services/edit-stale.js +358 -0
- package/mcp-server/dist/services/edit-stale.js.map +1 -0
- package/mcp-server/dist/services/embedding-service.d.ts +16 -0
- package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-service.js +126 -0
- package/mcp-server/dist/services/embedding-service.js.map +1 -0
- package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
- package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
- package/mcp-server/dist/services/embedding-worker.js +52 -0
- package/mcp-server/dist/services/embedding-worker.js.map +1 -0
- package/mcp-server/dist/services/fb-regions.d.ts +71 -0
- package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
- package/mcp-server/dist/services/fb-regions.js +211 -0
- package/mcp-server/dist/services/fb-regions.js.map +1 -0
- package/mcp-server/dist/services/file-checksums.d.ts +22 -0
- package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
- package/mcp-server/dist/services/file-checksums.js +55 -0
- package/mcp-server/dist/services/file-checksums.js.map +1 -0
- package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
- package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
- package/mcp-server/dist/services/file-excerpt.js +65 -0
- package/mcp-server/dist/services/file-excerpt.js.map +1 -0
- package/mcp-server/dist/services/identity.d.ts +124 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -0
- package/mcp-server/dist/services/identity.js +241 -0
- package/mcp-server/dist/services/identity.js.map +1 -0
- package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
- package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
- package/mcp-server/dist/services/local-embeddings.js +3 -0
- package/mcp-server/dist/services/local-embeddings.js.map +1 -0
- package/mcp-server/dist/services/memory-client.d.ts +4 -0
- package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-client.js +27 -0
- package/mcp-server/dist/services/memory-client.js.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
- package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
- package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-judgments.js +674 -0
- package/mcp-server/dist/services/memory-judgments.js.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
- package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-maintenance.js +403 -0
- package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
- package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-retrieval.js +412 -0
- package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
- package/mcp-server/dist/services/memory-schema.d.ts +16 -0
- package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-schema.js +282 -0
- package/mcp-server/dist/services/memory-schema.js.map +1 -0
- package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-sessions.js +110 -0
- package/mcp-server/dist/services/memory-sessions.js.map +1 -0
- package/mcp-server/dist/services/memory-triage.d.ts +60 -0
- package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-triage.js +189 -0
- package/mcp-server/dist/services/memory-triage.js.map +1 -0
- package/mcp-server/dist/services/memory-verification.d.ts +24 -0
- package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-verification.js +92 -0
- package/mcp-server/dist/services/memory-verification.js.map +1 -0
- package/mcp-server/dist/services/memory-writer.d.ts +26 -0
- package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
- package/mcp-server/dist/services/memory-writer.js +126 -0
- package/mcp-server/dist/services/memory-writer.js.map +1 -0
- package/mcp-server/dist/services/neural-interface.d.ts +439 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
- package/mcp-server/dist/services/neural-interface.js +1135 -0
- package/mcp-server/dist/services/neural-interface.js.map +1 -0
- package/mcp-server/dist/services/profiles.d.ts +120 -0
- package/mcp-server/dist/services/profiles.d.ts.map +1 -0
- package/mcp-server/dist/services/profiles.js +387 -0
- package/mcp-server/dist/services/profiles.js.map +1 -0
- package/mcp-server/dist/services/secret-gate.d.ts +29 -0
- package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
- package/mcp-server/dist/services/secret-gate.js +43 -0
- package/mcp-server/dist/services/secret-gate.js.map +1 -0
- package/mcp-server/dist/services/server-instructions.d.ts +10 -0
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
- package/mcp-server/dist/services/server-instructions.js +101 -0
- package/mcp-server/dist/services/server-instructions.js.map +1 -0
- package/mcp-server/dist/services/sqlite.d.ts +235 -0
- package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
- package/mcp-server/dist/services/sqlite.js +1185 -0
- package/mcp-server/dist/services/sqlite.js.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
- package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-bench.js +218 -0
- package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +746 -0
- package/mcp-server/dist/services/typesafe-config.js.map +1 -0
- package/mcp-server/dist/services/typesafe.d.ts +213 -0
- package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
- package/mcp-server/dist/services/typesafe.js +330 -0
- package/mcp-server/dist/services/typesafe.js.map +1 -0
- package/mcp-server/dist/services/vector-cache.d.ts +68 -0
- package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
- package/mcp-server/dist/services/vector-cache.js +271 -0
- package/mcp-server/dist/services/vector-cache.js.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
- package/mcp-server/dist/tools/agents.d.ts +212 -0
- package/mcp-server/dist/tools/agents.d.ts.map +1 -0
- package/mcp-server/dist/tools/agents.js +407 -0
- package/mcp-server/dist/tools/agents.js.map +1 -0
- package/mcp-server/dist/tools/bluesky.d.ts +39 -0
- package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
- package/mcp-server/dist/tools/bluesky.js +1144 -0
- package/mcp-server/dist/tools/bluesky.js.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-advanced.js +106 -0
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
- package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-batch.js +187 -0
- package/mcp-server/dist/tools/browser-batch.js.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
- package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-interact.js +300 -0
- package/mcp-server/dist/tools/browser-interact.js.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-navigate.js +142 -0
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
- package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser-observe.js +2009 -0
- package/mcp-server/dist/tools/browser-observe.js.map +1 -0
- package/mcp-server/dist/tools/browser.d.ts +34 -0
- package/mcp-server/dist/tools/browser.d.ts.map +1 -0
- package/mcp-server/dist/tools/browser.js +112 -0
- package/mcp-server/dist/tools/browser.js.map +1 -0
- package/mcp-server/dist/tools/card-tools.d.ts +82 -0
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/card-tools.js +137 -0
- package/mcp-server/dist/tools/card-tools.js.map +1 -0
- package/mcp-server/dist/tools/card.d.ts +8 -0
- package/mcp-server/dist/tools/card.d.ts.map +1 -0
- package/mcp-server/dist/tools/card.js +16 -0
- package/mcp-server/dist/tools/card.js.map +1 -0
- package/mcp-server/dist/tools/category.d.ts +32 -0
- package/mcp-server/dist/tools/category.d.ts.map +1 -0
- package/mcp-server/dist/tools/category.js +290 -0
- package/mcp-server/dist/tools/category.js.map +1 -0
- package/mcp-server/dist/tools/choice.d.ts +160 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -0
- package/mcp-server/dist/tools/choice.js +80 -0
- package/mcp-server/dist/tools/choice.js.map +1 -0
- package/mcp-server/dist/tools/computer.d.ts +71 -0
- package/mcp-server/dist/tools/computer.d.ts.map +1 -0
- package/mcp-server/dist/tools/computer.js +174 -0
- package/mcp-server/dist/tools/computer.js.map +1 -0
- package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-channel.js +236 -0
- package/mcp-server/dist/tools/discord-channel.js.map +1 -0
- package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-guild.js +138 -0
- package/mcp-server/dist/tools/discord-guild.js.map +1 -0
- package/mcp-server/dist/tools/discord-member.d.ts +26 -0
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-member.js +122 -0
- package/mcp-server/dist/tools/discord-member.js.map +1 -0
- package/mcp-server/dist/tools/discord-message.d.ts +36 -0
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-message.js +184 -0
- package/mcp-server/dist/tools/discord-message.js.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-onboarding.js +179 -0
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
- package/mcp-server/dist/tools/discord-role.d.ts +34 -0
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-role.js +178 -0
- package/mcp-server/dist/tools/discord-role.js.map +1 -0
- package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-thread.js +88 -0
- package/mcp-server/dist/tools/discord-thread.js.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord-webhook.js +154 -0
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
- package/mcp-server/dist/tools/discord.d.ts +7 -0
- package/mcp-server/dist/tools/discord.d.ts.map +1 -0
- package/mcp-server/dist/tools/discord.js +25 -0
- package/mcp-server/dist/tools/discord.js.map +1 -0
- package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/fb-groups.js +369 -0
- package/mcp-server/dist/tools/fb-groups.js.map +1 -0
- package/mcp-server/dist/tools/forget.d.ts +14 -0
- package/mcp-server/dist/tools/forget.d.ts.map +1 -0
- package/mcp-server/dist/tools/forget.js +26 -0
- package/mcp-server/dist/tools/forget.js.map +1 -0
- package/mcp-server/dist/tools/git-tools.d.ts +24 -0
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/git-tools.js +107 -0
- package/mcp-server/dist/tools/git-tools.js.map +1 -0
- package/mcp-server/dist/tools/git.d.ts +7 -0
- package/mcp-server/dist/tools/git.d.ts.map +1 -0
- package/mcp-server/dist/tools/git.js +11 -0
- package/mcp-server/dist/tools/git.js.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/experience.js +351 -0
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/helpers.js +258 -0
- package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/indexing.js +357 -0
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/inspect.js +321 -0
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/navigate.js +197 -0
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/performance.js +274 -0
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc/settings.js +425 -0
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
- package/mcp-server/dist/tools/gsc.d.ts +7 -0
- package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
- package/mcp-server/dist/tools/gsc.js +51 -0
- package/mcp-server/dist/tools/gsc.js.map +1 -0
- package/mcp-server/dist/tools/image-tools.d.ts +24 -0
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/image-tools.js +80 -0
- package/mcp-server/dist/tools/image-tools.js.map +1 -0
- package/mcp-server/dist/tools/image.d.ts +10 -0
- package/mcp-server/dist/tools/image.d.ts.map +1 -0
- package/mcp-server/dist/tools/image.js +14 -0
- package/mcp-server/dist/tools/image.js.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
- package/mcp-server/dist/tools/leonardo.d.ts +13 -0
- package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
- package/mcp-server/dist/tools/leonardo.js +21 -0
- package/mcp-server/dist/tools/leonardo.js.map +1 -0
- package/mcp-server/dist/tools/loop.d.ts +30 -0
- package/mcp-server/dist/tools/loop.d.ts.map +1 -0
- package/mcp-server/dist/tools/loop.js +401 -0
- package/mcp-server/dist/tools/loop.js.map +1 -0
- package/mcp-server/dist/tools/memories.d.ts +56 -0
- package/mcp-server/dist/tools/memories.d.ts.map +1 -0
- package/mcp-server/dist/tools/memories.js +171 -0
- package/mcp-server/dist/tools/memories.js.map +1 -0
- package/mcp-server/dist/tools/morelogin.d.ts +24 -0
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
- package/mcp-server/dist/tools/morelogin.js +79 -0
- package/mcp-server/dist/tools/morelogin.js.map +1 -0
- package/mcp-server/dist/tools/profile.d.ts +17 -0
- package/mcp-server/dist/tools/profile.d.ts.map +1 -0
- package/mcp-server/dist/tools/profile.js +114 -0
- package/mcp-server/dist/tools/profile.js.map +1 -0
- package/mcp-server/dist/tools/recall.d.ts +71 -0
- package/mcp-server/dist/tools/recall.d.ts.map +1 -0
- package/mcp-server/dist/tools/recall.js +147 -0
- package/mcp-server/dist/tools/recall.js.map +1 -0
- package/mcp-server/dist/tools/reflect.d.ts +56 -0
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
- package/mcp-server/dist/tools/reflect.js +157 -0
- package/mcp-server/dist/tools/reflect.js.map +1 -0
- package/mcp-server/dist/tools/remember.d.ts +62 -0
- package/mcp-server/dist/tools/remember.d.ts.map +1 -0
- package/mcp-server/dist/tools/remember.js +201 -0
- package/mcp-server/dist/tools/remember.js.map +1 -0
- package/mcp-server/dist/tools/response.d.ts +23 -0
- package/mcp-server/dist/tools/response.d.ts.map +1 -0
- package/mcp-server/dist/tools/response.js +24 -0
- package/mcp-server/dist/tools/response.js.map +1 -0
- package/mcp-server/dist/tools/restore.d.ts +14 -0
- package/mcp-server/dist/tools/restore.d.ts.map +1 -0
- package/mcp-server/dist/tools/restore.js +23 -0
- package/mcp-server/dist/tools/restore.js.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide-tools.js +231 -0
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
- package/mcp-server/dist/tools/style-guide.d.ts +8 -0
- package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
- package/mcp-server/dist/tools/style-guide.js +12 -0
- package/mcp-server/dist/tools/style-guide.js.map +1 -0
- package/mcp-server/dist/tools/sync.d.ts +20 -0
- package/mcp-server/dist/tools/sync.d.ts.map +1 -0
- package/mcp-server/dist/tools/sync.js +156 -0
- package/mcp-server/dist/tools/sync.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
- package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
- package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
- package/mcp-server/dist/tools/tictactoe.js +11 -0
- package/mcp-server/dist/tools/tictactoe.js.map +1 -0
- package/mcp-server/dist/tools/utils.d.ts +19 -0
- package/mcp-server/dist/tools/utils.d.ts.map +1 -0
- package/mcp-server/dist/tools/utils.js +54 -0
- package/mcp-server/dist/tools/utils.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.js +337 -0
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
- package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
- package/mcp-server/dist/tools/whiteboard.js +16 -0
- package/mcp-server/dist/tools/whiteboard.js.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/analytics.js +83 -0
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
- package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/dedup.js +56 -0
- package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
- package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/download.js +30 -0
- package/mcp-server/dist/tools/youtube/download.js.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
- package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/helpers.js +143 -0
- package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
- package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/media.js +97 -0
- package/mcp-server/dist/tools/youtube/media.js.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/metadata.js +106 -0
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/navigate.js +71 -0
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/playlist.js +128 -0
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/schedule.js +147 -0
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
- package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/source.js +85 -0
- package/mcp-server/dist/tools/youtube/source.js.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube/upload.js +255 -0
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
- package/mcp-server/dist/tools/youtube.d.ts +12 -0
- package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
- package/mcp-server/dist/tools/youtube.js +38 -0
- package/mcp-server/dist/tools/youtube.js.map +1 -0
- package/mcp-server/dist/tui.d.ts +3 -0
- package/mcp-server/dist/tui.d.ts.map +1 -0
- package/mcp-server/dist/tui.js +806 -0
- package/mcp-server/dist/tui.js.map +1 -0
- package/mcp-server/dist/types.d.ts +77 -0
- package/mcp-server/dist/types.d.ts.map +1 -0
- package/mcp-server/dist/types.js +2 -0
- package/mcp-server/dist/types.js.map +1 -0
- package/mcp-server/package-lock.json +4798 -0
- package/mcp-server/package.json +55 -38
- package/mcp-server/run.mjs +12 -2
- package/mcp-server/src/config.ts +48 -3
- package/mcp-server/src/http.ts +296 -93
- package/mcp-server/src/index.ts +546 -203
- package/mcp-server/src/preload-http.ts +19 -18
- package/mcp-server/src/preload.ts +19 -18
- package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
- package/mcp-server/src/services/assistant-judgments.ts +157 -0
- package/mcp-server/src/services/browser-assist-gate.ts +383 -0
- package/mcp-server/src/services/browser-assist.ts +267 -0
- package/mcp-server/src/services/browser-bench.ts +477 -0
- package/mcp-server/src/services/browser-judgments.ts +414 -0
- package/mcp-server/src/services/browser-risk.ts +484 -0
- package/mcp-server/src/services/categories.ts +36 -19
- package/mcp-server/src/services/codex-config-heal.ts +403 -0
- package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
- package/mcp-server/src/services/desktop-assist.ts +308 -0
- package/mcp-server/src/services/desktop-bench.ts +372 -0
- package/mcp-server/src/services/desktop-client.ts +76 -0
- package/mcp-server/src/services/desktop-judgments.ts +218 -0
- package/mcp-server/src/services/desktop-risk.ts +743 -0
- package/mcp-server/src/services/edit-stale.ts +299 -0
- package/mcp-server/src/services/embedding-service.ts +108 -0
- package/mcp-server/src/services/embedding-worker.ts +46 -0
- package/mcp-server/src/services/fb-regions.ts +221 -0
- package/mcp-server/src/services/file-checksums.ts +26 -9
- package/mcp-server/src/services/file-excerpt.ts +59 -0
- package/mcp-server/src/services/identity.ts +291 -0
- package/mcp-server/src/services/local-embeddings.ts +2 -77
- package/mcp-server/src/services/memory-client.ts +23 -0
- package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
- package/mcp-server/src/services/memory-judgments.ts +901 -0
- package/mcp-server/src/services/memory-maintenance.ts +360 -0
- package/mcp-server/src/services/memory-retrieval.ts +349 -0
- package/mcp-server/src/services/memory-schema.ts +280 -0
- package/mcp-server/src/services/memory-sessions.ts +87 -0
- package/mcp-server/src/services/memory-triage.ts +182 -0
- package/mcp-server/src/services/memory-verification.ts +68 -0
- package/mcp-server/src/services/memory-writer.ts +125 -0
- package/mcp-server/src/services/neural-interface.ts +1217 -73
- package/mcp-server/src/services/profiles.ts +428 -0
- package/mcp-server/src/services/secret-gate.ts +52 -0
- package/mcp-server/src/services/server-instructions.ts +122 -0
- package/mcp-server/src/services/sqlite.ts +1456 -823
- package/mcp-server/src/services/typesafe-bench.ts +247 -0
- package/mcp-server/src/services/typesafe-config.ts +790 -0
- package/mcp-server/src/services/typesafe.ts +390 -0
- package/mcp-server/src/services/vector-cache.ts +255 -0
- package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
- package/mcp-server/src/tools/agents.ts +539 -0
- package/mcp-server/src/tools/bluesky.ts +1186 -0
- package/mcp-server/src/tools/browser-advanced.ts +15 -6
- package/mcp-server/src/tools/browser-batch.ts +178 -0
- package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
- package/mcp-server/src/tools/browser-interact.ts +362 -255
- package/mcp-server/src/tools/browser-navigate.ts +164 -77
- package/mcp-server/src/tools/browser-observe.ts +2222 -1330
- package/mcp-server/src/tools/browser.ts +181 -107
- package/mcp-server/src/tools/card.ts +7 -5
- package/mcp-server/src/tools/choice.ts +103 -0
- package/mcp-server/src/tools/computer.ts +190 -0
- package/mcp-server/src/tools/discord.ts +10 -23
- package/mcp-server/src/tools/fb-groups.ts +387 -0
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git.ts +3 -1
- package/mcp-server/src/tools/gsc/experience.ts +440 -0
- package/mcp-server/src/tools/gsc/helpers.ts +288 -0
- package/mcp-server/src/tools/gsc/indexing.ts +495 -0
- package/mcp-server/src/tools/gsc/inspect.ts +391 -0
- package/mcp-server/src/tools/gsc/navigate.ts +236 -0
- package/mcp-server/src/tools/gsc/performance.ts +352 -0
- package/mcp-server/src/tools/gsc/settings.ts +564 -0
- package/mcp-server/src/tools/gsc.ts +95 -0
- package/mcp-server/src/tools/image.ts +3 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
- package/mcp-server/src/tools/leonardo.ts +7 -5
- package/mcp-server/src/tools/loop.ts +118 -25
- package/mcp-server/src/tools/memories.ts +65 -5
- package/mcp-server/src/tools/morelogin.ts +81 -0
- package/mcp-server/src/tools/profile.ts +122 -0
- package/mcp-server/src/tools/recall.ts +105 -261
- package/mcp-server/src/tools/reflect.ts +171 -145
- package/mcp-server/src/tools/remember.ts +223 -115
- package/mcp-server/src/tools/style-guide-tools.ts +250 -0
- package/mcp-server/src/tools/style-guide.ts +16 -0
- package/mcp-server/src/tools/sync.ts +66 -9
- package/mcp-server/src/tools/tictactoe.ts +3 -1
- package/mcp-server/src/tools/utils.ts +22 -0
- package/mcp-server/src/tools/whiteboard-format.ts +394 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
- package/mcp-server/src/tools/whiteboard.ts +7 -5
- package/mcp-server/src/tools/youtube/analytics.ts +106 -0
- package/mcp-server/src/tools/youtube/dedup.ts +57 -0
- package/mcp-server/src/tools/youtube/download.ts +41 -0
- package/mcp-server/src/tools/youtube/helpers.ts +183 -0
- package/mcp-server/src/tools/youtube/media.ts +109 -0
- package/mcp-server/src/tools/youtube/metadata.ts +112 -0
- package/mcp-server/src/tools/youtube/navigate.ts +81 -0
- package/mcp-server/src/tools/youtube/playlist.ts +134 -0
- package/mcp-server/src/tools/youtube/schedule.ts +158 -0
- package/mcp-server/src/tools/youtube/source.ts +101 -0
- package/mcp-server/src/tools/youtube/upload.ts +296 -0
- package/mcp-server/src/tools/youtube.ts +47 -0
- package/mcp-server/src/tui.ts +8 -9
- package/neural-interface/games/TicTacToe/1.svg +1 -0
- package/neural-interface/games/TicTacToe/2.svg +1 -0
- package/neural-interface/games/TicTacToe/3.svg +1 -0
- package/neural-interface/games/TicTacToe/4.svg +1 -0
- package/neural-interface/games/TicTacToe/5.svg +1 -0
- package/neural-interface/games/TicTacToe/6.svg +1 -0
- package/neural-interface/games/TicTacToe/7.svg +1 -0
- package/neural-interface/games/TicTacToe/8.svg +1 -0
- package/neural-interface/games/TicTacToe/9.svg +1 -0
- package/neural-interface/games/TicTacToe/Board.svg +1 -0
- package/neural-interface/games/TicTacToe/Circle.svg +1 -0
- package/neural-interface/games/TicTacToe/Cross.svg +1 -0
- package/neural-interface/i18n/en.json +7022 -0
- package/neural-interface/i18n/pt-BR.json +7026 -0
- package/neural-interface/lib/assistant-api.js +1005 -0
- package/neural-interface/lib/assistant-attachments.js +121 -0
- package/neural-interface/lib/assistant-brains/claude.js +207 -0
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
- package/neural-interface/lib/assistant-brains/codex.js +213 -0
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
- package/neural-interface/lib/assistant-brains/opencode.js +320 -0
- package/neural-interface/lib/assistant-budget.js +318 -0
- package/neural-interface/lib/assistant-catalog.js +731 -0
- package/neural-interface/lib/assistant-clarify.js +664 -0
- package/neural-interface/lib/assistant-config.js +232 -0
- package/neural-interface/lib/assistant-dispatch.js +2744 -0
- package/neural-interface/lib/assistant-envelope.js +582 -0
- package/neural-interface/lib/assistant-evidence.js +236 -0
- package/neural-interface/lib/assistant-hidden-models.js +272 -0
- package/neural-interface/lib/assistant-jev.js +117 -0
- package/neural-interface/lib/assistant-judgments.js +111 -0
- package/neural-interface/lib/assistant-media.js +335 -0
- package/neural-interface/lib/assistant-memory.js +398 -0
- package/neural-interface/lib/assistant-persona.js +444 -0
- package/neural-interface/lib/assistant-plan-permissions.js +448 -0
- package/neural-interface/lib/assistant-playbooks/design.md +87 -0
- package/neural-interface/lib/assistant-playbooks.js +72 -0
- package/neural-interface/lib/assistant-pricing.js +159 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
- package/neural-interface/lib/assistant-route-gate.js +212 -0
- package/neural-interface/lib/assistant-router.js +1324 -0
- package/neural-interface/lib/assistant-runtime.js +3337 -0
- package/neural-interface/lib/assistant-task-prompt.js +472 -0
- package/neural-interface/lib/assistant-usage.js +1688 -0
- package/neural-interface/lib/augmented-path.js +70 -0
- package/neural-interface/lib/automation-storage-migration.js +54 -0
- package/neural-interface/lib/backup-service.js +851 -0
- package/neural-interface/lib/backup-zip-reader.js +262 -0
- package/neural-interface/lib/browser-assist-api.js +41 -0
- package/neural-interface/lib/browser-console.js +176 -0
- package/neural-interface/lib/browser-execution.js +241 -0
- package/neural-interface/lib/browser-extraction.js +191 -0
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/browser-screenshot.js +399 -0
- package/neural-interface/lib/browser-semantic-context.js +639 -0
- package/neural-interface/lib/browser-snapshots.js +192 -0
- package/neural-interface/lib/browser-tab-recovery.js +119 -0
- package/neural-interface/lib/browser-targets.js +70 -0
- package/neural-interface/lib/browser-tool-policy.js +926 -0
- package/neural-interface/lib/claude-accounts.js +230 -0
- package/neural-interface/lib/claude-agent-bridge.js +3203 -0
- package/neural-interface/lib/claude-bypass-policy.js +99 -0
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-history.js +429 -0
- package/neural-interface/lib/claude-model-catalog.js +444 -0
- package/neural-interface/lib/claude-panel-requests.js +165 -0
- package/neural-interface/lib/claude-panel-session.js +272 -0
- package/neural-interface/lib/claude-permission-rules.js +153 -0
- package/neural-interface/lib/claude-session-cleanup.js +71 -0
- package/neural-interface/lib/claude-session-list.js +82 -0
- package/neural-interface/lib/claude-session-ops.js +140 -0
- package/neural-interface/lib/claude-settings-view.js +51 -0
- package/neural-interface/lib/claude-skin-connect.js +79 -0
- package/neural-interface/lib/claude-stall-policy.js +7 -0
- package/neural-interface/lib/claude-temporary.js +435 -0
- package/neural-interface/lib/cli-update-planner.js +312 -0
- package/neural-interface/lib/codex-capabilities.d.ts +41 -0
- package/neural-interface/lib/codex-capabilities.js +148 -0
- package/neural-interface/lib/codex-collaboration-mode.js +17 -0
- package/neural-interface/lib/codex-compat-rpc.js +241 -0
- package/neural-interface/lib/codex-mcp-approvals.js +56 -0
- package/neural-interface/lib/codex-mcp-apps.js +47 -0
- package/neural-interface/lib/codex-model-context.js +202 -0
- package/neural-interface/lib/codex-plan-store.js +71 -0
- package/neural-interface/lib/codex-request-journal.js +242 -0
- package/neural-interface/lib/codex-runtime-path.js +213 -0
- package/neural-interface/lib/codex-session-catalog.js +231 -0
- package/neural-interface/lib/codex-session-settings.js +46 -0
- package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
- package/neural-interface/lib/codex-thread-history.js +165 -0
- package/neural-interface/lib/codex-user-verification.js +34 -0
- package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
- package/neural-interface/lib/confined-fs.js +256 -0
- package/neural-interface/lib/db.js +922 -0
- package/neural-interface/lib/design-md.js +9 -0
- package/neural-interface/lib/desktop/api.js +92 -0
- package/neural-interface/lib/desktop/audit.js +62 -0
- package/neural-interface/lib/desktop/build.js +423 -0
- package/neural-interface/lib/desktop/config.js +606 -0
- package/neural-interface/lib/desktop/coords.js +76 -0
- package/neural-interface/lib/desktop/fake-helper.js +1192 -0
- package/neural-interface/lib/desktop/frames.js +77 -0
- package/neural-interface/lib/desktop/grants.js +84 -0
- package/neural-interface/lib/desktop/guards.js +170 -0
- package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
- package/neural-interface/lib/desktop/lease.js +51 -0
- package/neural-interface/lib/desktop/manager.js +545 -0
- package/neural-interface/lib/desktop/press-contexts.js +103 -0
- package/neural-interface/lib/desktop/protocol.js +495 -0
- package/neural-interface/lib/desktop/service.js +1202 -0
- package/neural-interface/lib/effort-levels.js +148 -0
- package/neural-interface/lib/event-loop-monitor.js +67 -0
- package/neural-interface/lib/exec-loop-command.js +209 -0
- package/neural-interface/lib/folder-picker.ps1 +77 -0
- package/neural-interface/lib/http-guards.js +287 -0
- package/neural-interface/lib/jsonl-lines.js +94 -0
- package/neural-interface/lib/loop-logger.js +127 -0
- package/neural-interface/lib/mcp-client-config.js +133 -0
- package/neural-interface/lib/mcp-installer.js +487 -0
- package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
- package/neural-interface/lib/mcp-profile-servers.js +120 -0
- package/neural-interface/lib/memo-cache.js +84 -0
- package/neural-interface/lib/memory-api.js +758 -0
- package/neural-interface/lib/memory-map-api.js +1150 -0
- package/neural-interface/lib/memory-map-layout.js +872 -0
- package/neural-interface/lib/memory-map-worker.js +165 -0
- package/neural-interface/lib/memory-session-capture.js +81 -0
- package/neural-interface/lib/memory-timers.js +62 -0
- package/neural-interface/lib/morelogin.js +301 -0
- package/neural-interface/lib/native-binary-runtime.js +382 -0
- package/neural-interface/lib/native-loop-goal.js +33 -0
- package/neural-interface/lib/native-loop-providers.js +1002 -0
- package/neural-interface/lib/native-loop-runtime.js +747 -0
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/opencode-hidden-models.js +113 -0
- package/neural-interface/lib/opencode-history.js +253 -0
- package/neural-interface/lib/opencode-runtime-config.js +58 -0
- package/neural-interface/lib/opencode-v2-client.js +486 -0
- package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
- package/neural-interface/lib/path-confine.js +213 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/lib/project-storage.js +766 -0
- package/neural-interface/lib/pty-host/core.js +603 -0
- package/neural-interface/lib/pty-host/flow.js +43 -0
- package/neural-interface/lib/pty-host/host.js +47 -0
- package/neural-interface/lib/pty-host/manager.js +360 -0
- package/neural-interface/lib/pty-host/protocol.js +81 -0
- package/neural-interface/lib/pty-host/snapshot.js +133 -0
- package/neural-interface/lib/remote-policy.js +582 -0
- package/neural-interface/lib/rulesets/api.js +175 -0
- package/neural-interface/lib/rulesets/fs-safe.js +245 -0
- package/neural-interface/lib/rulesets/installer.js +1453 -0
- package/neural-interface/lib/rulesets/json-edit.js +186 -0
- package/neural-interface/lib/rulesets/lock.js +170 -0
- package/neural-interface/lib/rulesets/managed-text.js +314 -0
- package/neural-interface/lib/rulesets/normalise.js +24 -0
- package/neural-interface/lib/rulesets/render.js +84 -0
- package/neural-interface/lib/schedule-launch-intent.js +40 -0
- package/neural-interface/lib/session-cache-builder.js +524 -0
- package/neural-interface/lib/session-chunker.js +344 -0
- package/neural-interface/lib/session-indexer.js +327 -0
- package/neural-interface/lib/session-title-generator.js +232 -0
- package/neural-interface/lib/skill-prompt-bridge.js +11 -0
- package/neural-interface/lib/style-guide/api.js +375 -0
- package/neural-interface/lib/style-guide/color.js +352 -0
- package/neural-interface/lib/style-guide/export-css.js +99 -0
- package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
- package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
- package/neural-interface/lib/style-guide/fonts.json +227 -0
- package/neural-interface/lib/style-guide/import.js +1268 -0
- package/neural-interface/lib/style-guide/pointers.js +103 -0
- package/neural-interface/lib/style-guide/presets.js +232 -0
- package/neural-interface/lib/style-guide/render-design-md.js +319 -0
- package/neural-interface/lib/style-guide/schema.js +848 -0
- package/neural-interface/lib/style-guide/security.js +43 -0
- package/neural-interface/lib/style-guide/store.js +491 -0
- package/neural-interface/lib/style-guide/summary.js +106 -0
- package/neural-interface/lib/style-guide/tokens.js +170 -0
- package/neural-interface/lib/synabun-update-plan.js +324 -0
- package/neural-interface/lib/synabun-version.js +179 -0
- package/neural-interface/lib/system-restore.js +99 -0
- package/neural-interface/lib/terminal-env.js +86 -0
- package/neural-interface/lib/ui-blob-store.js +106 -0
- package/neural-interface/lib/vector-cache.js +2 -0
- package/neural-interface/lib/virtual-socket.js +60 -0
- package/neural-interface/lib/whatsapp/api.js +146 -0
- package/neural-interface/lib/whatsapp/auth-store.js +389 -0
- package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
- package/neural-interface/lib/whatsapp/brain.js +128 -0
- package/neural-interface/lib/whatsapp/bridge.js +1499 -0
- package/neural-interface/lib/whatsapp/cards.js +519 -0
- package/neural-interface/lib/whatsapp/commands.js +92 -0
- package/neural-interface/lib/whatsapp/config.js +291 -0
- package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
- package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
- package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
- package/neural-interface/lib/whatsapp/connector/package.json +8 -0
- package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
- package/neural-interface/lib/whatsapp/format.js +552 -0
- package/neural-interface/lib/whatsapp/host-core.js +1225 -0
- package/neural-interface/lib/whatsapp/host.js +119 -0
- package/neural-interface/lib/whatsapp/identity.js +288 -0
- package/neural-interface/lib/whatsapp/inbound.js +149 -0
- package/neural-interface/lib/whatsapp/installer.js +562 -0
- package/neural-interface/lib/whatsapp/manager.js +638 -0
- package/neural-interface/lib/whatsapp/paths.js +81 -0
- package/neural-interface/lib/whatsapp/protocol.js +327 -0
- package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
- package/neural-interface/lib/whatsapp/redact.js +130 -0
- package/neural-interface/lib/whatsapp/service.js +1305 -0
- package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
- package/neural-interface/package-lock.json +4431 -0
- package/neural-interface/package.json +58 -27
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +1 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index.html +32 -182
- package/neural-interface/public/index2d.html +2 -2
- package/neural-interface/public/install-app.html +31 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/mcp.svg +4 -0
- package/neural-interface/public/offline.html +670 -299
- package/neural-interface/public/onboarding.html +804 -161
- package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
- package/neural-interface/public/shared/agent-runtime-options.js +596 -0
- package/neural-interface/public/shared/api.js +339 -28
- package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
- package/neural-interface/public/shared/assistant/asst-api.js +270 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
- package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
- package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
- package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
- package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +313 -0
- package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +586 -0
- package/neural-interface/public/shared/assistant/asst-render.js +3305 -0
- package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
- package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
- package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
- package/neural-interface/public/shared/assistant/asst-state.js +727 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +1686 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
- package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
- package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
- package/neural-interface/public/shared/automation-context.js +75 -0
- package/neural-interface/public/shared/blob-store.js +99 -0
- package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
- package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
- package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
- package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
- package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
- package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
- package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
- package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
- package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
- package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
- package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
- package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
- package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
- package/neural-interface/public/shared/cli-status.js +169 -0
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/constants.js +22 -5
- package/neural-interface/public/shared/cp/cp-agents.js +186 -0
- package/neural-interface/public/shared/cp/cp-bash.js +252 -0
- package/neural-interface/public/shared/cp/cp-compose.js +159 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
- package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
- package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
- package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
- package/neural-interface/public/shared/cp/cp-diff.js +255 -0
- package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
- package/neural-interface/public/shared/cp/cp-events.js +846 -0
- package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
- package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
- package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
- package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
- package/neural-interface/public/shared/cp/cp-restore.js +224 -0
- package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
- package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
- package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
- package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
- package/neural-interface/public/shared/cp/cp-styles.js +349 -0
- package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
- package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
- package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
- package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
- package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
- package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
- package/neural-interface/public/shared/graph-activity.js +68 -0
- package/neural-interface/public/shared/html-shell.js +116 -17
- package/neural-interface/public/shared/i18n.js +132 -34
- package/neural-interface/public/shared/node-positions-store.js +44 -0
- package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
- package/neural-interface/public/shared/provider-icons.js +30 -0
- package/neural-interface/public/shared/pwa-install-page.js +34 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +28 -0
- package/neural-interface/public/shared/session-title.js +100 -0
- package/neural-interface/public/shared/settings/settings-ia.js +389 -0
- package/neural-interface/public/shared/settings/settings-kit.js +234 -0
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/settings/settings-search.js +463 -0
- package/neural-interface/public/shared/settings/settings-shell.js +570 -0
- package/neural-interface/public/shared/start-bridge.js +583 -0
- package/neural-interface/public/shared/state.js +5 -0
- package/neural-interface/public/shared/storage.js +50 -7
- package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
- package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +151 -0
- package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
- package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
- package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
- package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-import-export.js +48 -0
- package/neural-interface/public/shared/styleguide/sg-kit.js +284 -0
- package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
- package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
- package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +237 -0
- package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
- package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
- package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
- package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
- package/neural-interface/public/shared/styleguide/sg-typography.js +125 -0
- package/neural-interface/public/shared/styles.css +8614 -1477
- package/neural-interface/public/shared/synabun-mascot.js +1625 -0
- package/neural-interface/public/shared/synabun-ticker.js +376 -0
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/term-renderer.js +343 -0
- package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
- package/neural-interface/public/shared/ui-assistant.js +77 -0
- package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
- package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
- package/neural-interface/public/shared/ui-codex-panel.js +12 -0
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-cost-widget.js +55 -34
- package/neural-interface/public/shared/ui-detail.js +77 -14
- package/neural-interface/public/shared/ui-explorer.js +360 -181
- package/neural-interface/public/shared/ui-file-explorer.js +485 -107
- package/neural-interface/public/shared/ui-help.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +45 -21
- package/neural-interface/public/shared/ui-judgments.js +1346 -0
- package/neural-interface/public/shared/ui-keybinds.js +3 -2
- package/neural-interface/public/shared/ui-link.js +105 -51
- package/neural-interface/public/shared/ui-loading.js +222 -11
- package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
- package/neural-interface/public/shared/ui-menubar.js +100 -31
- package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
- package/neural-interface/public/shared/ui-native-window-id.js +7 -0
- package/neural-interface/public/shared/ui-navbar.js +357 -222
- package/neural-interface/public/shared/ui-notifications.js +58 -12
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
- package/neural-interface/public/shared/ui-panels.js +15 -7
- package/neural-interface/public/shared/ui-resume.js +823 -653
- package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
- package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
- package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
- package/neural-interface/public/shared/ui-sessions.js +789 -34
- package/neural-interface/public/shared/ui-settings.js +5774 -2194
- package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
- package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
- package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
- package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
- package/neural-interface/public/shared/ui-skills.js +18 -32
- package/neural-interface/public/shared/ui-styleguide.js +35 -0
- package/neural-interface/public/shared/ui-sync.js +87 -0
- package/neural-interface/public/shared/ui-terminal.js +7863 -6385
- package/neural-interface/public/shared/ui-tool-updates.js +292 -0
- package/neural-interface/public/shared/ui-tooltip.js +156 -15
- package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
- package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
- package/neural-interface/public/shared/ui-tutorial.css +102 -0
- package/neural-interface/public/shared/ui-tutorial.js +557 -1076
- package/neural-interface/public/shared/ui-update.js +420 -68
- package/neural-interface/public/shared/ui-whatsapp.js +941 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
- package/neural-interface/public/shared/ui-workspaces.js +2 -1
- package/neural-interface/public/shared/utils.js +58 -0
- package/neural-interface/public/shared/vterm-buffer.js +15 -5
- package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
- package/neural-interface/public/sidepanel-session.html +18 -0
- package/neural-interface/public/sw.js +10 -2
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/variant/2d/graph.js +1707 -1635
- package/neural-interface/public/variant/2d/lasso.js +2 -1
- package/neural-interface/public/variant/2d/layout.js +595 -594
- package/neural-interface/public/variant/2d/main.js +557 -521
- package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
- package/neural-interface/public/variant/3d/main.js +517 -500
- package/neural-interface/public/variant/3d/map-controls.js +144 -0
- package/neural-interface/public/variant/3d/map-data.js +251 -0
- package/neural-interface/public/variant/3d/map-labels.js +241 -0
- package/neural-interface/public/variant/3d/map-pick.js +90 -0
- package/neural-interface/public/variant/3d/map-renderer.js +521 -0
- package/neural-interface/public/variant/3d/map-shaders.js +158 -0
- package/neural-interface/public/variant/3d/map-view.js +618 -0
- package/neural-interface/public/vendor/three/manifest.json +3 -0
- package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
- package/neural-interface/public/vendor/xterm/manifest.json +3 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
- package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
- package/neural-interface/scripts/build-vendor.mjs +64 -0
- package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
- package/neural-interface/scripts/rebuild-pty.js +39 -22
- package/neural-interface/scripts/vendor-three-entry.js +24 -0
- package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
- package/neural-interface/server.js +33983 -15000
- package/neural-interface/skins/default/skin.css +90 -0
- package/neural-interface/skins/default/skin.json +8 -0
- package/neural-interface/templates/rulesets/README.md +9 -0
- package/neural-interface/templates/rulesets/claude.md +3 -0
- package/neural-interface/templates/rulesets/codex.md +2 -0
- package/neural-interface/templates/rulesets/coexistence.md +6 -0
- package/neural-interface/templates/rulesets/core.md +26 -0
- package/neural-interface/templates/rulesets/cursor.md +2 -0
- package/neural-interface/templates/rulesets/gemini.md +2 -0
- package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
- package/neural-interface/templates/rulesets/manifest.json +12 -0
- package/neural-interface/templates/rulesets/opencode.md +2 -0
- package/package.json +131 -53
- package/postinstall.js +55 -26
- package/preuninstall.js +187 -0
- package/scripts/restore-data-home-from-backup.mjs +239 -0
- package/setup.js +872 -260
- package/skills/leonardo/SKILL.md +19 -10
- package/skills/leonardo/modules/image-prompter.md +18 -12
- package/skills/leonardo/modules/video-prompter.md +19 -13
- package/skills/synabun/SKILL.md +209 -185
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +380 -374
- package/skills/synabun/modules/changelog.md +15 -6
- package/skills/synabun/modules/idea.md +148 -144
- package/skills/synabun/modules/memorize.md +13 -4
- package/skills/synabun/modules/schedule.md +198 -0
- package/skills/synabun/synabunicon.png +0 -0
- package/updater.cmd +31 -0
- package/updater.mjs +382 -0
- package/updater.sh +29 -0
- package/hooks/claude-code/test-user-learning.mjs +0 -531
- package/neural-interface/public/claude-chat.css +0 -878
- package/neural-interface/public/claude-chat.html +0 -92
- package/neural-interface/public/claude-chat.js +0 -1168
- package/neural-interface/public/index-legacy.html +0 -14677
- package/neural-interface/public/index2d-legacy.html +0 -8193
- package/neural-interface/public/shared/html-term-renderer.js +0 -1022
- package/neural-interface/public/variant/3d/background.js +0 -884
- package/neural-interface/public/variant/3d/camera.js +0 -661
- package/neural-interface/public/variant/3d/gfx.js +0 -116
- package/neural-interface/public/variant/3d/graph.js +0 -2399
- package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
- package/neural-interface/templates/CLAUDE-template.md +0 -211
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — WhatsApp Link: the bridge between the owner's chat and the Assistant
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Owner messages become Assistant turns (runtime.submit, origin 'whatsapp');
|
|
6
|
+
// what the Assistant says, asks and decides comes back as WhatsApp text. The
|
|
7
|
+
// bridge only observes the runtime (runtime.observe: never a socket, so the
|
|
8
|
+
// panel's replay buffer and idle disposal stay the panel's) and answers
|
|
9
|
+
// through the same paths as the panel (answerControl, answerDispatchControl,
|
|
10
|
+
// stopTurn) with origin 'whatsapp'.
|
|
11
|
+
//
|
|
12
|
+
// Turns, per session: IDLE → SUBMITTING → RUNNING(wa) → FINALIZE for the
|
|
13
|
+
// owner's messages; a synabun.mailbox event is RUNNING(mailbox), a turn the
|
|
14
|
+
// CLI starts itself RUNNING(background), and a turn nobody here submitted (a
|
|
15
|
+
// prompt typed on the desktop) RUNNING(external) — tracked, never forwarded.
|
|
16
|
+
// The turn object exists before submit is called: brain events can arrive
|
|
17
|
+
// before it resolves.
|
|
18
|
+
//
|
|
19
|
+
// Any message carries the conversation on. While something waits for the
|
|
20
|
+
// owner (a question, an approval, a route, a plan, a worker's request), a
|
|
21
|
+
// clear answer answers it (cards.js); anything else is never an answer to
|
|
22
|
+
// wait behind: a question takes the owner's words as its answer, an approval
|
|
23
|
+
// closes WITHOUT being granted (denied, cancelled) and the message goes to the
|
|
24
|
+
// Assistant as the next prompt. An approval is only ever granted by a "yes"
|
|
25
|
+
// the bridge matched on a message the owner typed, aimed at that request while
|
|
26
|
+
// it is open: never by the model, by a forwarded message, by a reply that
|
|
27
|
+
// quotes something else or by anything that merely looks like an answer.
|
|
28
|
+
// Messages that arrive while a turn runs wait as one merged prompt and go in
|
|
29
|
+
// as soon as it ends; a queue watch lets go of a turn whose end never came.
|
|
30
|
+
// A request that appears while such a message waits closes at once, unseen and
|
|
31
|
+
// without a grant: text written before a request existed never answers it.
|
|
32
|
+
//
|
|
33
|
+
// Computer use: the owner's switch (config.computerUse) rides on the policy of
|
|
34
|
+
// the current conversation only; a retired conversation never drives the Mac.
|
|
35
|
+
// At Ask the runtime raises one "control your Mac for this?" request per turn:
|
|
36
|
+
// it is answered like every other approval here (a plain yes from the owner's
|
|
37
|
+
// phone grants it for that turn; anything else closes it without a grant).
|
|
38
|
+
// A route card for a computer task done here carries that question itself
|
|
39
|
+
// (cards.js routeControlsMac): one yes, and no second request in that turn.
|
|
40
|
+
// Computer control stopped at the Mac is told to the phone in one line.
|
|
41
|
+
//
|
|
42
|
+
// Sessions: "WhatsApp · <Mon D>", remote (lib/remote-policy.js) at the
|
|
43
|
+
// configured level, on the brain Settings → WhatsApp names (lib/whatsapp/
|
|
44
|
+
// brain.js; a changed choice switches the session's brain before the next
|
|
45
|
+
// message, like the panel's own model switch), rotated per config.rotation or
|
|
46
|
+
// /new — never silently on a budget refusal. Up to 3 retired sessions keep
|
|
47
|
+
// forwarding late results for 24 h. Transport, config, formatting and inbound
|
|
48
|
+
// composition are injected (Packages A and C); nothing here knows a phone number.
|
|
49
|
+
|
|
50
|
+
import { assistantEventKey } from '../../public/shared/assistant/asst-state.js';
|
|
51
|
+
import { brainCapsLevel, defaultRemotePolicyRegistry, effectiveLevel, remoteBrainNotice } from '../remote-policy.js';
|
|
52
|
+
import { brainSignature, resolveBrainChoice } from './brain.js';
|
|
53
|
+
import { answerCard, cardFromPacket, failClosedResponse, isBareAnswer, randomTag, renderCard } from './cards.js';
|
|
54
|
+
import { HELP_TEXT, SETTINGS_REFUSAL, parseCommand, statusText } from './commands.js';
|
|
55
|
+
|
|
56
|
+
const MINUTE = 60_000;
|
|
57
|
+
const HOUR = 60 * MINUTE;
|
|
58
|
+
|
|
59
|
+
export const BRIDGE_LIMITS = Object.freeze({
|
|
60
|
+
onItMs: 10_000, // "On it…" when a turn is still running after this
|
|
61
|
+
stillFirstMs: 90_000, // first "Still working…"
|
|
62
|
+
stillEveryMs: 5 * MINUTE, // then every this long…
|
|
63
|
+
stillMax: 3, // …at most this many times
|
|
64
|
+
narrationGapMs: 20_000, // progress 'all': narration at least this far apart
|
|
65
|
+
dispatchBatchMs: 30_000, // "Started agent: …" notes are batched this long
|
|
66
|
+
typingEveryMs: 20_000, // typing presence refresh
|
|
67
|
+
drainMs: 300, // the next queued message goes this long after a turn ends
|
|
68
|
+
busyRetryMs: 5_000, // a busy refusal retries after this when no turn end comes
|
|
69
|
+
watchdogMs: 30 * MINUTE, // a turn nobody hears the end of is let go
|
|
70
|
+
letGoMs: 20_000, // a turn the runtime no longer runs, silent this long, is over (its end was missed)
|
|
71
|
+
queueWatchMs: 5_000, // while messages wait, the queue is tried again this often
|
|
72
|
+
queueMax: 5,
|
|
73
|
+
mergeChars: 12_000, // messages that wait together go in as one prompt, up to this much text…
|
|
74
|
+
mergeImages: 4, // …and this many pictures
|
|
75
|
+
staleMs: 10 * MINUTE, // older when received: not acted on
|
|
76
|
+
staleNoticeMs: 3_000, // the "skipped N older messages" note waits for the burst to end
|
|
77
|
+
unsupportedEveryMs: 10 * MINUTE,
|
|
78
|
+
perMinute: 20,
|
|
79
|
+
perDay: 300,
|
|
80
|
+
idleRotateMs: 6 * HOUR,
|
|
81
|
+
retiredKeepMs: 24 * HOUR,
|
|
82
|
+
retiredMax: 3,
|
|
83
|
+
forwardPerHour: 6,
|
|
84
|
+
desktopAnsweredMs: 10 * MINUTE, // a number sent this soon after the desktop answered a card: "Already answered"
|
|
85
|
+
outboxMax: 20,
|
|
86
|
+
throttleRetryMs: 30_000, // a THROTTLED send without a retryAfterMs is tried again after this
|
|
87
|
+
policyTickMs: 30_000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** Send refusals that mean "not now": the reply waits in the outbox for the next connect. */
|
|
91
|
+
const RESEND_ON_CONNECT = /DISCONNECT|NOT_CONNECTED|OFFLINE|RECONNECT|CLOSED|TIMEOUT|HOST_UNAVAILABLE/i;
|
|
92
|
+
/**
|
|
93
|
+
* What happens to a reply the transport refused: 'connect' (it waits for the
|
|
94
|
+
* next connect), 'later' (THROTTLED: sent again after its retryAfterMs) or
|
|
95
|
+
* 'drop'. PAUSED (an owner anomaly paused sending) and HELD (the connector
|
|
96
|
+
* kept crashing) drop it: the service records why in the activity log and the
|
|
97
|
+
* Settings tab shows the state.
|
|
98
|
+
*/
|
|
99
|
+
export function outboxPlan(code) {
|
|
100
|
+
const value = String(code || '');
|
|
101
|
+
if (value === 'THROTTLED') return 'later';
|
|
102
|
+
return RESEND_ON_CONNECT.test(value) ? 'connect' : 'drop';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
106
|
+
enabled: true, level: 'ask', progress: 'key', forwardBackground: true, replyLabel: null, maxMessages: 3,
|
|
107
|
+
rotation: 'idle6h', strictWorkerApprovals: false, computerUse: false, mode: 'self', paused: false, pausedBy: null, autonomousUntil: null,
|
|
108
|
+
});
|
|
109
|
+
const ALREADY_ANSWERED = new Set(['CONTROL_UNKNOWN', 'ROUTE_ALREADY_DECIDED', 'ROUTE_NOT_FOUND', 'CLARIFY_ALREADY_ANSWERED', 'CLARIFY_NOT_FOUND', 'PERMISSION_ALREADY_RESOLVED', 'NO_PENDING_REQUEST', 'RUN_NOT_FOUND']);
|
|
110
|
+
const ROUTE_DONE_PHASES = new Set(['decided', 'auto', 'expired', 'declined', 'cancelled']);
|
|
111
|
+
// What a worker is told when its request closes because the owner wrote something else.
|
|
112
|
+
const WORKER_MOVED_ON_NOTE = 'The user did not approve this on WhatsApp (they moved on to something else). Do not retry it: continue without it, or report it as blocked.';
|
|
113
|
+
const CLARIFY_DONE_PHASES = new Set(['answered', 'declined', 'chat', 'cancelled', 'expired']);
|
|
114
|
+
// Computer control was stopped at the Mac (Esc, the corner, the Stop button): one line to the phone.
|
|
115
|
+
export const MAC_STOP_TEXT = 'Computer control was stopped on your Mac: I am no longer controlling it.';
|
|
116
|
+
const usd = (value) => `$${(Number(value) || 0).toFixed(2)}`;
|
|
117
|
+
const hhmm = (ms) => new Date(ms).toTimeString().slice(0, 5);
|
|
118
|
+
const monthDay = (ms) => new Date(ms).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
119
|
+
function tsMs(ts, fallback) { const n = Number(ts); if (!Number.isFinite(n) || n <= 0) return fallback; return n < 1e12 ? n * 1000 : n; }
|
|
120
|
+
/** The most recent 04:00 (local) at or before `ms`. */
|
|
121
|
+
function lastFourAm(ms) { const d = new Date(ms); d.setHours(4, 0, 0, 0); if (d.getTime() > ms) d.setDate(d.getDate() - 1); return d.getTime(); }
|
|
122
|
+
|
|
123
|
+
const globalTimers = {
|
|
124
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
125
|
+
clearTimeout: (id) => clearTimeout(id),
|
|
126
|
+
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
127
|
+
clearInterval: (id) => clearInterval(id),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {object} deps
|
|
132
|
+
* @param {() => object} deps.getRuntime the Assistant runtime (createAssistantRuntime)
|
|
133
|
+
* @param {() => object} [deps.getDispatcher] the dispatcher (runs of the session, /stop)
|
|
134
|
+
* @param {object} deps.transport { send(text,{replyTo}) → {ok,id}|{ok:false,code}, react(id, name|null), presence(state), markRead(ids), connected() }
|
|
135
|
+
* @param {object} deps.config { read(), write(servicePatch) } (whatsapp_config)
|
|
136
|
+
* @param {object} deps.format { toWhatsApp(md), chunk(text, {max, maxChunks, prefix, tail}) }
|
|
137
|
+
* @param {object} deps.inbound { composePrompt(parts, {rng}), createCoalescer({windowMs, maxImages, onFlush, timers}) }
|
|
138
|
+
* @param {object} [deps.policy] lib/remote-policy.js registry
|
|
139
|
+
* @param {() => object|null} [deps.getDefaultBrain] the brain the panel would start (its last used brain); null → the Assistant's default
|
|
140
|
+
* @param {() => object|null} [deps.getCatalog] the Assistant's model catalog ({ peek() }): checks the configured WhatsApp brain
|
|
141
|
+
* @param {object} [deps.limits] BRIDGE_LIMITS overrides (tests)
|
|
142
|
+
*/
|
|
143
|
+
export function createWhatsAppBridge({
|
|
144
|
+
getRuntime,
|
|
145
|
+
getDispatcher = () => null,
|
|
146
|
+
transport,
|
|
147
|
+
config,
|
|
148
|
+
format,
|
|
149
|
+
inbound,
|
|
150
|
+
policy = defaultRemotePolicyRegistry,
|
|
151
|
+
now = Date.now,
|
|
152
|
+
timers = globalTimers,
|
|
153
|
+
log = () => {},
|
|
154
|
+
getDefaultBrain = null,
|
|
155
|
+
getCatalog = null,
|
|
156
|
+
rng = Math.random,
|
|
157
|
+
limits: limitOverrides = {},
|
|
158
|
+
// The capability that says "this is the owner's phone" (lib/remote-policy.js createPhoneAuthority),
|
|
159
|
+
// handed over once by whoever builds the bridge. It lives in this closure only: never in `state`,
|
|
160
|
+
// a card, a log line or anything returned. Presented where a grant can happen, nowhere else.
|
|
161
|
+
authority: phoneAuthority = null,
|
|
162
|
+
} = {}) {
|
|
163
|
+
if (typeof getRuntime !== 'function') throw new Error('createWhatsAppBridge requires getRuntime()');
|
|
164
|
+
if (!transport?.send) throw new Error('createWhatsAppBridge requires transport.send');
|
|
165
|
+
if (!config?.read) throw new Error('createWhatsAppBridge requires config.read');
|
|
166
|
+
const L = { ...BRIDGE_LIMITS, ...(limitOverrides || {}) };
|
|
167
|
+
const T = { ...globalTimers, ...(timers || {}) };
|
|
168
|
+
|
|
169
|
+
const startCfg = readConfig();
|
|
170
|
+
const bridgeState = startCfg.bridgeState && typeof startCfg.bridgeState === 'object' ? startCfg.bridgeState : {};
|
|
171
|
+
const state = {
|
|
172
|
+
sessionId: startCfg.sessionId || null,
|
|
173
|
+
previous: [], // [{ id, until }] retired sessions whose results still forward
|
|
174
|
+
queue: [], // prompt jobs waiting for the Assistant
|
|
175
|
+
turns: new Map(), // sessionId → turn
|
|
176
|
+
cards: new Map(), // key → card, in arrival order
|
|
177
|
+
currentKey: null,
|
|
178
|
+
cardByMessage: new Map(), // WhatsApp message id → card key (quote-replies)
|
|
179
|
+
closedCards: new Map(), // key → { at, by } (recently closed)
|
|
180
|
+
lastDesktopAnswerAt: 0,
|
|
181
|
+
paused: startCfg.paused === true,
|
|
182
|
+
pausedBy: startCfg.pausedBy || null,
|
|
183
|
+
pausedNoticeSent: false,
|
|
184
|
+
lastReplyAt: null,
|
|
185
|
+
lastInboundId: null,
|
|
186
|
+
lastActivityAt: Number(bridgeState.lastActivityAt) || null,
|
|
187
|
+
rotateOnNextSubmit: false,
|
|
188
|
+
inflight: bridgeState.inflight && typeof bridgeState.inflight === 'object' ? bridgeState.inflight : null,
|
|
189
|
+
restartNotice: bridgeState.inflight || (Array.isArray(bridgeState.queuedIds) && bridgeState.queuedIds.length) ? { inflight: bridgeState.inflight || null, queued: (bridgeState.queuedIds || []).length } : null,
|
|
190
|
+
outbox: [],
|
|
191
|
+
outboxTimer: null, // a THROTTLED reply's retry
|
|
192
|
+
staleSkipped: 0,
|
|
193
|
+
staleTimer: null,
|
|
194
|
+
unsupportedAt: new Map(),
|
|
195
|
+
minuteWindow: { start: 0, count: 0, noticed: false },
|
|
196
|
+
dayWindow: { start: 0, count: 0, noticed: false },
|
|
197
|
+
forwards: [], // timestamps of forwarded updates (last hour)
|
|
198
|
+
forwardSuppressed: 0,
|
|
199
|
+
forwardSummaryTimer: null,
|
|
200
|
+
dispatchBatch: [],
|
|
201
|
+
dispatchTimer: null,
|
|
202
|
+
drainTimer: null,
|
|
203
|
+
busyRetryTimer: null,
|
|
204
|
+
policyTimer: null,
|
|
205
|
+
policySeen: new Map(), // sessionId → { signature, effective }
|
|
206
|
+
followUp: null, // { sessionId, at }: a card the phone saw was just decided
|
|
207
|
+
queueTimer: null, // tries the queue again while messages wait
|
|
208
|
+
// { sessionId, signature }: the configured brain choice the session was last put on (brain.js).
|
|
209
|
+
brainApplied: bridgeState.brainApplied && typeof bridgeState.brainApplied === 'object' ? { sessionId: String(bridgeState.brainApplied.sessionId || ''), signature: String(bridgeState.brainApplied.signature || 'assistant') } : null,
|
|
210
|
+
starting: false, // a job is between "not busy" and its turn: the next one waits
|
|
211
|
+
flushed: null, // the prompt a coalescer flushed while it was pushed
|
|
212
|
+
buffered: 0, // messages inside the coalescer window (a coalescer without size())
|
|
213
|
+
// "<session>:<provider>" the owner was told runs read-only because of its brain (once each).
|
|
214
|
+
brainNotices: new Set(Array.isArray(bridgeState.brainNotices) ? bridgeState.brainNotices.map(String).slice(-20) : []),
|
|
215
|
+
stopped: false,
|
|
216
|
+
};
|
|
217
|
+
const retired = bridgeState.retired && typeof bridgeState.retired === 'object' ? bridgeState.retired : {};
|
|
218
|
+
for (const id of Array.isArray(startCfg.previousSessionIds) ? startCfg.previousSessionIds : []) {
|
|
219
|
+
const sid = typeof id === 'string' ? id : id?.id;
|
|
220
|
+
if (!sid) continue;
|
|
221
|
+
const until = Number(retired[sid]) || Number(id?.until) || (now() + L.retiredKeepMs);
|
|
222
|
+
if (until > now()) state.previous.push({ id: sid, until });
|
|
223
|
+
}
|
|
224
|
+
let runtimeRef = null;
|
|
225
|
+
let unobserve = null;
|
|
226
|
+
let unsubscribeRuntime = null;
|
|
227
|
+
let coalescer = null;
|
|
228
|
+
|
|
229
|
+
// ── config ─────────────────────────────────────────────────────────────────
|
|
230
|
+
function readConfig() {
|
|
231
|
+
let value = {};
|
|
232
|
+
try { value = config.read() || {}; } catch (error) { log('whatsapp:bridge', `config read failed: ${error?.message || error}`); }
|
|
233
|
+
return { ...DEFAULT_CONFIG, ...value };
|
|
234
|
+
}
|
|
235
|
+
function writeConfig(patch) {
|
|
236
|
+
try { const result = config.write?.(patch); if (result && typeof result.catch === 'function') result.catch(() => {}); }
|
|
237
|
+
catch (error) { log('whatsapp:bridge', `config write failed: ${error?.message || error}`); }
|
|
238
|
+
}
|
|
239
|
+
function persistBridgeState() {
|
|
240
|
+
const retiredMap = Object.fromEntries(state.previous.map((row) => [row.id, row.until]));
|
|
241
|
+
writeConfig({
|
|
242
|
+
bridgeState: {
|
|
243
|
+
inflight: state.inflight || null,
|
|
244
|
+
queuedIds: state.queue.flatMap((job) => job.sourceIds || []),
|
|
245
|
+
lastActivityAt: state.lastActivityAt || null,
|
|
246
|
+
retired: retiredMap,
|
|
247
|
+
brainNotices: [...state.brainNotices].slice(-20),
|
|
248
|
+
brainApplied: state.brainApplied || null,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── runtime attachment ─────────────────────────────────────────────────────
|
|
254
|
+
function runtime() {
|
|
255
|
+
let rt = null;
|
|
256
|
+
try { rt = getRuntime() || null; } catch { rt = null; }
|
|
257
|
+
if (rt && rt !== runtimeRef) {
|
|
258
|
+
try { unobserve?.(); } catch {}
|
|
259
|
+
try { unsubscribeRuntime?.(); } catch {}
|
|
260
|
+
runtimeRef = rt;
|
|
261
|
+
unobserve = typeof rt.observe === 'function' ? rt.observe((message) => { try { onPacket(message); } catch (error) { log('whatsapp:bridge', `packet: ${error?.message || error}`); } }) : null;
|
|
262
|
+
unsubscribeRuntime = typeof rt.subscribe === 'function' ? rt.subscribe((message) => { try { onRuntimeEvent(message); } catch {} }) : null;
|
|
263
|
+
syncPolicies();
|
|
264
|
+
}
|
|
265
|
+
return rt;
|
|
266
|
+
}
|
|
267
|
+
function dispatcher() { try { return getDispatcher?.() || null; } catch { return null; } }
|
|
268
|
+
function ours(sessionId) {
|
|
269
|
+
if (!sessionId) return false;
|
|
270
|
+
if (sessionId === state.sessionId) return true;
|
|
271
|
+
return state.previous.some((row) => row.id === sessionId && row.until > now());
|
|
272
|
+
}
|
|
273
|
+
function sessionView(id) {
|
|
274
|
+
const rt = runtime();
|
|
275
|
+
if (!rt || !id) return null;
|
|
276
|
+
try { return rt.getSession?.(id, { transcript: false }) || null; } catch { return null; }
|
|
277
|
+
}
|
|
278
|
+
/** The session's policy as the runtime sees it (its brainProvider included), else the registry's. */
|
|
279
|
+
function policyOf(sessionId) {
|
|
280
|
+
const rt = runtime();
|
|
281
|
+
try { return rt?.sessionPolicy?.(sessionId) || policy?.getSessionPolicy?.(sessionId) || null; } catch { return null; }
|
|
282
|
+
}
|
|
283
|
+
function levelOf(sessionId) {
|
|
284
|
+
const current = policyOf(sessionId);
|
|
285
|
+
return current ? effectiveLevel(current, { now: now() }) : readConfig().level;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Ask and Autonomous hold on a Claude brain only: a conversation on Codex or
|
|
289
|
+
* OpenCode runs read-only, and the owner is told why once (per brain).
|
|
290
|
+
*/
|
|
291
|
+
async function noticeBrainLimit(sessionId) {
|
|
292
|
+
const current = policyOf(sessionId);
|
|
293
|
+
const provider = current?.brainProvider || sessionView(sessionId)?.brain?.provider || null;
|
|
294
|
+
if (!current || !provider || !brainCapsLevel(current, { provider, now: now() })) return;
|
|
295
|
+
const key = `${sessionId}:${provider}`;
|
|
296
|
+
if (state.brainNotices.has(key)) return;
|
|
297
|
+
state.brainNotices.add(key);
|
|
298
|
+
persistBridgeState();
|
|
299
|
+
await say(remoteBrainNotice(provider), { maxChunks: 1 });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── sending ────────────────────────────────────────────────────────────────
|
|
303
|
+
function prefix() {
|
|
304
|
+
const cfg = readConfig();
|
|
305
|
+
if (cfg.replyLabel) return String(cfg.replyLabel).trim();
|
|
306
|
+
return cfg.mode === 'dedicated' ? '' : 'SynaBun:';
|
|
307
|
+
}
|
|
308
|
+
function isConnected() { try { return transport.connected ? transport.connected() !== false : true; } catch { return false; } }
|
|
309
|
+
/**
|
|
310
|
+
* Text to the owner, split per config. `markdown`: the Assistant's own words
|
|
311
|
+
* (converted to WhatsApp formatting); `lead`: a line of ours put before them as is.
|
|
312
|
+
* → the sent message ids.
|
|
313
|
+
*/
|
|
314
|
+
async function say(text, { replyTo = null, markdown = false, maxChunks = null, lead = '', cardKey = null } = {}) {
|
|
315
|
+
const body = String(text ?? '').trim();
|
|
316
|
+
if (!body) return [];
|
|
317
|
+
const cfg = readConfig();
|
|
318
|
+
let converted = body;
|
|
319
|
+
if (markdown && typeof format?.toWhatsApp === 'function') { try { converted = format.toWhatsApp(body) || body; } catch { converted = body; } }
|
|
320
|
+
if (lead) converted = `${lead}\n${converted}`;
|
|
321
|
+
let chunks = [converted];
|
|
322
|
+
if (typeof format?.chunk === 'function') {
|
|
323
|
+
try {
|
|
324
|
+
chunks = format.chunk(converted, { max: 3500, maxChunks: maxChunks || Number(cfg.maxMessages) || 3, prefix: prefix() || undefined, tail: '… (the rest is in SynaBun on your computer)' }) || [converted];
|
|
325
|
+
} catch { chunks = [converted]; }
|
|
326
|
+
} else if (prefix()) {
|
|
327
|
+
chunks = [`${prefix()} ${converted}`];
|
|
328
|
+
}
|
|
329
|
+
const ids = [];
|
|
330
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
331
|
+
const id = await sendRaw(chunks[i], { replyTo: i === 0 ? replyTo : null, cardKey });
|
|
332
|
+
if (id) ids.push(id);
|
|
333
|
+
}
|
|
334
|
+
if (ids.length) { state.lastReplyAt = now(); state.lastActivityAt = now(); }
|
|
335
|
+
return ids;
|
|
336
|
+
}
|
|
337
|
+
/** `cardKey`: the text is a request's message; it keeps that identity while it waits in the outbox. */
|
|
338
|
+
async function sendRaw(text, { replyTo = null, cardKey = null } = {}) {
|
|
339
|
+
if (!isConnected()) { queueOutbox(text, replyTo, null, null, cardKey); return null; }
|
|
340
|
+
try {
|
|
341
|
+
const result = await transport.send(text, replyTo ? { replyTo } : {});
|
|
342
|
+
// Delivered: from here on the request it carries can be answered (replyToPending), and a quote of it names it.
|
|
343
|
+
if (result?.ok) { if (cardKey && result.id) mapCardMessage(cardKey, result.id); return result.id || null; }
|
|
344
|
+
if (result && result.ok === false) queueOutbox(text, replyTo, result.code, result.retryAfterMs, cardKey);
|
|
345
|
+
} catch (error) { log('whatsapp:bridge', `send failed: ${error?.message || error}`); queueOutbox(text, replyTo, null, null, cardKey); }
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
/** → true when the reply is waiting in the outbox, false when it was dropped (outboxPlan). */
|
|
349
|
+
function queueOutbox(text, replyTo, code = null, retryAfterMs = null, cardKey = null) {
|
|
350
|
+
const plan = code ? outboxPlan(code) : 'connect';
|
|
351
|
+
if (plan === 'drop') return false;
|
|
352
|
+
state.outbox.push({ text, replyTo, ...(cardKey ? { cardKey } : {}) });
|
|
353
|
+
if (state.outbox.length > L.outboxMax) state.outbox.splice(0, state.outbox.length - L.outboxMax);
|
|
354
|
+
if (plan === 'later') armOutboxRetry(retryAfterMs);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
/** THROTTLED: try the outbox again once the send caps free up (a connect flushes it sooner). */
|
|
358
|
+
function armOutboxRetry(retryAfterMs) {
|
|
359
|
+
if (state.outboxTimer || state.stopped) return;
|
|
360
|
+
const wait = Number.isFinite(Number(retryAfterMs)) && Number(retryAfterMs) > 0 ? Number(retryAfterMs) + 250 : L.throttleRetryMs;
|
|
361
|
+
state.outboxTimer = T.setTimeout(() => {
|
|
362
|
+
state.outboxTimer = null;
|
|
363
|
+
if (!state.stopped && isConnected()) flushOutbox().catch((error) => log('whatsapp:bridge', `outbox: ${error?.message || error}`));
|
|
364
|
+
}, wait);
|
|
365
|
+
state.outboxTimer?.unref?.();
|
|
366
|
+
}
|
|
367
|
+
async function flushOutbox() {
|
|
368
|
+
if (state.outboxTimer) { T.clearTimeout(state.outboxTimer); state.outboxTimer = null; }
|
|
369
|
+
const pending = state.outbox.splice(0, state.outbox.length);
|
|
370
|
+
for (let i = 0; i < pending.length; i += 1) {
|
|
371
|
+
const { text, replyTo, cardKey = null } = pending[i];
|
|
372
|
+
// A request that closed while its message waited is not asked late.
|
|
373
|
+
if (cardKey && !state.cards.has(cardKey)) continue;
|
|
374
|
+
const before = state.outbox.length;
|
|
375
|
+
const id = await sendRaw(text, { replyTo, cardKey });
|
|
376
|
+
// Delivered by the retry: a reply that quotes it is aimed at that request, like one sent at once.
|
|
377
|
+
if (id && cardKey) mapCardMessage(cardKey, id);
|
|
378
|
+
// Refused again and back in line (offline, throttled): the rest keeps its order behind it.
|
|
379
|
+
if (state.outbox.length > before) {
|
|
380
|
+
state.outbox.push(...pending.slice(i + 1));
|
|
381
|
+
if (state.outbox.length > L.outboxMax) state.outbox.splice(0, state.outbox.length - L.outboxMax);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function react(ids, name) {
|
|
387
|
+
for (const id of ids || []) { try { const r = transport.react?.(id, name); if (r?.catch) r.catch(() => {}); } catch {} }
|
|
388
|
+
}
|
|
389
|
+
function presence(value) { try { const r = transport.presence?.(value); if (r?.catch) r.catch(() => {}); } catch {} }
|
|
390
|
+
|
|
391
|
+
// ── sessions ───────────────────────────────────────────────────────────────
|
|
392
|
+
function defaultBrain() {
|
|
393
|
+
let brain = null;
|
|
394
|
+
try { brain = typeof getDefaultBrain === 'function' ? getDefaultBrain() : null; } catch { brain = null; }
|
|
395
|
+
return brain && typeof brain === 'object' ? { ...brain } : {};
|
|
396
|
+
}
|
|
397
|
+
/** The brain Settings → WhatsApp names now: the stored choice, else the panel's (a disabled or unknown choice falls back). */
|
|
398
|
+
function brainTarget() {
|
|
399
|
+
let catalog = null;
|
|
400
|
+
try { catalog = (typeof getCatalog === 'function' ? getCatalog() : null)?.peek?.() || null; } catch { catalog = null; }
|
|
401
|
+
return resolveBrainChoice(readConfig().brain || null, { catalog, panelBrain: defaultBrain() });
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The choice changed since this session was last put on it: switch the
|
|
405
|
+
* session's brain before its next turn (runtime.updateSession, what the
|
|
406
|
+
* panel's model switch calls). A model or effort change keeps the provider's
|
|
407
|
+
* conversation; another provider starts its own inside the same session.
|
|
408
|
+
* "Same as the Assistant" is read when a conversation starts and when the
|
|
409
|
+
* setting changes: the panel picking another model later does not move a
|
|
410
|
+
* conversation that is under way.
|
|
411
|
+
*/
|
|
412
|
+
async function applyBrainChoice(sessionId) {
|
|
413
|
+
const target = brainTarget();
|
|
414
|
+
const signature = brainSignature(target);
|
|
415
|
+
const applied = state.brainApplied?.sessionId === sessionId ? state.brainApplied.signature : 'assistant';
|
|
416
|
+
if (signature === applied) return;
|
|
417
|
+
const rt = runtime();
|
|
418
|
+
const want = target.brain || {};
|
|
419
|
+
const have = sessionView(sessionId)?.brain || {};
|
|
420
|
+
// The effort is part of the choice in both directions: an explicit effort must not outlive
|
|
421
|
+
// the choice when "Same as the Assistant" names the same model at another (or the default) effort.
|
|
422
|
+
const effortOf = (brain) => (brain.effort && brain.effort !== 'off' ? brain.effort : null);
|
|
423
|
+
const differs = want.provider && (want.provider !== have.provider || (want.model && want.model !== have.model) || effortOf(want) !== effortOf(have));
|
|
424
|
+
if (differs && rt?.updateSession) {
|
|
425
|
+
try {
|
|
426
|
+
// "off" is the explicit reset to the model's default effort (a missing effort would keep the old one).
|
|
427
|
+
await rt.updateSession(sessionId, { brain: { provider: want.provider, ...(want.model ? { model: want.model } : {}), effort: effortOf(want) || 'off' } });
|
|
428
|
+
log('whatsapp:bridge', `session ${sessionId}: brain set to ${want.provider}${want.model ? `/${want.model}` : ''} (${target.source === 'choice' ? 'the WhatsApp setting' : 'same as the Assistant'})`);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
// Not applied (the model was disabled meanwhile, the runtime refused): the conversation stays on its brain; tried again at the next message.
|
|
431
|
+
log('whatsapp:bridge', `session ${sessionId}: brain not switched: ${error?.message || error}`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
state.brainApplied = { sessionId, signature };
|
|
436
|
+
persistBridgeState();
|
|
437
|
+
}
|
|
438
|
+
function remoteFromConfig(cfg = readConfig()) {
|
|
439
|
+
return { channel: 'whatsapp', level: cfg.level || 'ask', strictWorkerApprovals: cfg.strictWorkerApprovals === true, autonomousUntil: cfg.autonomousUntil || null, paused: state.paused, computerUse: cfg.computerUse === true && state.computerEnded !== true };
|
|
440
|
+
}
|
|
441
|
+
/** The policy of one of our sessions: computer use is the current conversation's alone. */
|
|
442
|
+
function remoteFor(id, base = remoteFromConfig()) {
|
|
443
|
+
return id === state.sessionId ? base : { ...base, computerUse: false };
|
|
444
|
+
}
|
|
445
|
+
const policySignature = (remote) => JSON.stringify([remote.level, remote.strictWorkerApprovals, remote.autonomousUntil, remote.paused, remote.computerUse]);
|
|
446
|
+
async function createSession() {
|
|
447
|
+
const rt = runtime();
|
|
448
|
+
if (!rt?.createSession) throw Object.assign(new Error('The Assistant is not available right now.'), { code: 'RUNTIME_UNAVAILABLE' });
|
|
449
|
+
const target = brainTarget();
|
|
450
|
+
const session = await rt.createSession(
|
|
451
|
+
{ brain: target.brain, label: `WhatsApp · ${monthDay(now())}`, fallback: true, channel: 'whatsapp' },
|
|
452
|
+
{ remote: remoteFromConfig() },
|
|
453
|
+
);
|
|
454
|
+
state.sessionId = session.id;
|
|
455
|
+
state.brainApplied = { sessionId: session.id, signature: brainSignature(target) };
|
|
456
|
+
state.policySeen.delete(session.id);
|
|
457
|
+
syncPolicies();
|
|
458
|
+
writeConfig({ sessionId: session.id, previousSessionIds: state.previous.map((row) => row.id) });
|
|
459
|
+
persistBridgeState();
|
|
460
|
+
log('whatsapp:bridge', `session ${session.id} started`);
|
|
461
|
+
return session.id;
|
|
462
|
+
}
|
|
463
|
+
function retire(id) {
|
|
464
|
+
if (!id) return;
|
|
465
|
+
state.previous = [{ id, until: now() + L.retiredKeepMs }, ...state.previous.filter((row) => row.id !== id && row.until > now())].slice(0, L.retiredMax);
|
|
466
|
+
// Cards of a retired conversation are no longer the phone's to answer.
|
|
467
|
+
for (const card of [...state.cards.values()]) if (card.sessionId === id) closeCard(card, 'retired', { quiet: true });
|
|
468
|
+
// …and it stops driving the Mac now, not at the next sync: its policy loses computer use at once.
|
|
469
|
+
if (policy?.registerSessionPolicy) {
|
|
470
|
+
const remote = { ...remoteFromConfig(), computerUse: false };
|
|
471
|
+
try { policy.registerSessionPolicy(id, remote); state.policySeen.set(id, { signature: policySignature(remote), effective: effectiveLevel({ ...remote }, { now: now() }) }); }
|
|
472
|
+
catch (error) { log('whatsapp:bridge', `policy ${id}: ${error?.message || error}`); state.policySeen.delete(id); }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function newSession(reason = 'command') {
|
|
476
|
+
const old = state.sessionId;
|
|
477
|
+
retire(old);
|
|
478
|
+
state.sessionId = null;
|
|
479
|
+
state.rotateOnNextSubmit = false;
|
|
480
|
+
const id = await createSession();
|
|
481
|
+
log('whatsapp:bridge', `new session (${reason})${old ? `, retired ${old}` : ''}`);
|
|
482
|
+
return id;
|
|
483
|
+
}
|
|
484
|
+
function rotationDue(activityBefore) {
|
|
485
|
+
const cfg = readConfig();
|
|
486
|
+
if (!state.sessionId || !activityBefore) return false;
|
|
487
|
+
if (cfg.rotation === 'never') return false;
|
|
488
|
+
if (cfg.rotation === 'daily') return activityBefore < lastFourAm(now());
|
|
489
|
+
return now() - activityBefore > L.idleRotateMs;
|
|
490
|
+
}
|
|
491
|
+
/** The session the next prompt goes to (created, or rotated, when needed). */
|
|
492
|
+
async function ensureSession() {
|
|
493
|
+
const view = state.sessionId ? sessionView(state.sessionId) : null;
|
|
494
|
+
if (state.sessionId && (!view || view.status === 'ended')) { state.sessionId = null; }
|
|
495
|
+
if (state.sessionId && state.rotateOnNextSubmit) { retire(state.sessionId); state.sessionId = null; }
|
|
496
|
+
state.rotateOnNextSubmit = false;
|
|
497
|
+
return state.sessionId || createSession();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── policy ─────────────────────────────────────────────────────────────────
|
|
501
|
+
/**
|
|
502
|
+
* Keep the level of every WhatsApp session in line with the config (level,
|
|
503
|
+
* strict approvals, the autonomous window, pause). A change — or an
|
|
504
|
+
* autonomous window that just ran out — re-registers, which makes the
|
|
505
|
+
* runtime re-apply the clamp and stop runs above it.
|
|
506
|
+
*/
|
|
507
|
+
function syncPolicies() {
|
|
508
|
+
const cfg = readConfig();
|
|
509
|
+
applyPause(cfg);
|
|
510
|
+
if (!policy?.registerSessionPolicy) return;
|
|
511
|
+
const ids = [state.sessionId, ...state.previous.filter((row) => row.until > now()).map((row) => row.id)].filter(Boolean);
|
|
512
|
+
const base = remoteFromConfig(cfg);
|
|
513
|
+
for (const id of ids) {
|
|
514
|
+
const remote = remoteFor(id, base);
|
|
515
|
+
const signature = policySignature(remote);
|
|
516
|
+
const effective = effectiveLevel({ ...remote }, { now: now() });
|
|
517
|
+
const seen = state.policySeen.get(id);
|
|
518
|
+
const registered = policy.getSessionPolicy?.(id);
|
|
519
|
+
if (seen && seen.signature === signature && seen.effective === effective && registered && !registered.failClosed) continue;
|
|
520
|
+
try { policy.registerSessionPolicy(id, remote); } catch (error) { log('whatsapp:bridge', `policy ${id}: ${error?.message || error}`); continue; }
|
|
521
|
+
state.policySeen.set(id, { signature, effective });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** Pause lives in the config (Settings can pause and resume too): follow it. */
|
|
525
|
+
function applyPause(cfg) {
|
|
526
|
+
const paused = cfg.paused === true;
|
|
527
|
+
if (paused !== state.paused) {
|
|
528
|
+
state.paused = paused;
|
|
529
|
+
state.pausedBy = paused ? (cfg.pausedBy || 'desktop') : null;
|
|
530
|
+
state.pausedNoticeSent = false;
|
|
531
|
+
if (!paused && state.queue.length) scheduleDrain(L.drainMs);
|
|
532
|
+
} else if (paused && cfg.pausedBy) {
|
|
533
|
+
state.pausedBy = cfg.pausedBy;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── turns ──────────────────────────────────────────────────────────────────
|
|
538
|
+
function makeTurn(sessionId, kind, extra = {}) {
|
|
539
|
+
const turn = {
|
|
540
|
+
sessionId, kind, state: kind === 'wa' ? 'SUBMITTING' : 'RUNNING', startedAt: now(), lastPacketAt: now(), sourceIds: [], replyTo: null,
|
|
541
|
+
segments: [], seen: new Set(), resultText: null, resultError: null, endedEarly: null, repliedProgress: false,
|
|
542
|
+
lastProgressAt: 0, stillCount: 0, timers: new Set(), intervals: new Set(), memoryOnly: false, stoppedByPhone: false,
|
|
543
|
+
...extra,
|
|
544
|
+
};
|
|
545
|
+
state.turns.set(sessionId, turn);
|
|
546
|
+
arm(turn, L.watchdogMs, () => { if (state.turns.get(sessionId) === turn) finalizeTurn(sessionId, 'watchdog'); });
|
|
547
|
+
return turn;
|
|
548
|
+
}
|
|
549
|
+
function arm(turn, ms, fn) {
|
|
550
|
+
const id = T.setTimeout(() => { turn.timers.delete(id); try { fn(); } catch (error) { log('whatsapp:bridge', `timer: ${error?.message || error}`); } }, ms);
|
|
551
|
+
turn.timers.add(id);
|
|
552
|
+
return id;
|
|
553
|
+
}
|
|
554
|
+
function disarm(turn) {
|
|
555
|
+
for (const id of turn.timers) T.clearTimeout(id);
|
|
556
|
+
for (const id of turn.intervals) T.clearInterval(id);
|
|
557
|
+
turn.timers.clear();
|
|
558
|
+
turn.intervals.clear();
|
|
559
|
+
}
|
|
560
|
+
function ensureTurn(sessionId, kind = 'external') {
|
|
561
|
+
return state.turns.get(sessionId) || makeTurn(sessionId, kind === 'external' && followUpFor(sessionId) ? 'followup' : kind);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* A route or clarify card of the phone's was just decided: the turn that
|
|
565
|
+
* carries the task on (a continuation, the mailbox) answers the phone.
|
|
566
|
+
*/
|
|
567
|
+
function followUpFor(sessionId) {
|
|
568
|
+
const f = state.followUp;
|
|
569
|
+
if (!f || f.sessionId !== sessionId) return null;
|
|
570
|
+
if (now() - f.at > 5 * MINUTE) { state.followUp = null; return null; }
|
|
571
|
+
return f;
|
|
572
|
+
}
|
|
573
|
+
/** The progress notes and the typing presence of a turn the owner is waiting for. */
|
|
574
|
+
function startProgress(turn) {
|
|
575
|
+
const cfg = readConfig();
|
|
576
|
+
presence('composing');
|
|
577
|
+
const typing = T.setInterval(() => { if (state.turns.get(turn.sessionId) === turn) presence('composing'); }, L.typingEveryMs);
|
|
578
|
+
turn.intervals.add(typing);
|
|
579
|
+
if (cfg.progress === 'off') return;
|
|
580
|
+
// Nothing to say while the turn waits for the owner's own answer on a card.
|
|
581
|
+
arm(turn, L.onItMs, () => {
|
|
582
|
+
if (state.turns.get(turn.sessionId) !== turn || turn.repliedProgress || hasOpenCards(turn.sessionId)) return;
|
|
583
|
+
turn.repliedProgress = true;
|
|
584
|
+
turn.lastProgressAt = now();
|
|
585
|
+
say('On it…', { maxChunks: 1 });
|
|
586
|
+
});
|
|
587
|
+
const still = () => {
|
|
588
|
+
if (state.turns.get(turn.sessionId) !== turn || turn.stillCount >= L.stillMax) return;
|
|
589
|
+
if (!hasOpenCards(turn.sessionId)) {
|
|
590
|
+
turn.stillCount += 1;
|
|
591
|
+
turn.lastProgressAt = now();
|
|
592
|
+
say('Still working…', { maxChunks: 1 });
|
|
593
|
+
}
|
|
594
|
+
if (turn.stillCount < L.stillMax) arm(turn, L.stillEveryMs, still);
|
|
595
|
+
};
|
|
596
|
+
arm(turn, L.stillFirstMs, still);
|
|
597
|
+
}
|
|
598
|
+
function addSegment(turn, event) {
|
|
599
|
+
const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
600
|
+
const text = blocks.filter((block) => block?.type === 'text' && block.text).map((block) => block.text).join('\n').trim();
|
|
601
|
+
if (!text) return;
|
|
602
|
+
// progress 'all': the previous top-level segment was narration.
|
|
603
|
+
const cfg = readConfig();
|
|
604
|
+
if (turn.kind === 'wa' && cfg.progress === 'all' && turn.segments.length && now() - turn.lastProgressAt >= L.narrationGapMs) {
|
|
605
|
+
const previous = turn.segments[turn.segments.length - 1];
|
|
606
|
+
if (previous && !turn.narrated?.has(previous)) {
|
|
607
|
+
turn.narrated = turn.narrated || new Set();
|
|
608
|
+
turn.narrated.add(previous);
|
|
609
|
+
turn.lastProgressAt = now();
|
|
610
|
+
turn.repliedProgress = true;
|
|
611
|
+
say(previous, { markdown: true, maxChunks: 1 });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
turn.segments.push(text);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The answer to send: the result's text, else the last text blocks. "Smart":
|
|
618
|
+
* a short closing line after an earlier long segment carries that segment too.
|
|
619
|
+
*/
|
|
620
|
+
function finalText(turn) {
|
|
621
|
+
const segments = turn.segments.filter(Boolean);
|
|
622
|
+
const result = typeof turn.resultText === 'string' ? turn.resultText.trim() : '';
|
|
623
|
+
const final = result || segments[segments.length - 1] || '';
|
|
624
|
+
if (!final) return '';
|
|
625
|
+
if (final.length < 200) {
|
|
626
|
+
const earlier = segments.filter((segment) => segment !== final && segment.length >= 280);
|
|
627
|
+
const lead = earlier[earlier.length - 1];
|
|
628
|
+
if (lead && !turn.narrated?.has(lead)) return `${lead}\n\n${final}`;
|
|
629
|
+
}
|
|
630
|
+
return final;
|
|
631
|
+
}
|
|
632
|
+
function closeBrainCards(sessionId) {
|
|
633
|
+
for (const card of [...state.cards.values()]) {
|
|
634
|
+
if (card.sessionId === sessionId && card.source === 'brain') closeCard(card, 'turn_ended', { quiet: true });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function finalizeTurn(sessionId, outcome = 'done', { message = null } = {}) {
|
|
638
|
+
const turn = state.turns.get(sessionId);
|
|
639
|
+
if (!turn) return;
|
|
640
|
+
if (turn.state === 'SUBMITTING' && outcome !== 'watchdog') { turn.endedEarly = { outcome, message }; return; }
|
|
641
|
+
state.turns.delete(sessionId);
|
|
642
|
+
turn.state = 'FINALIZE';
|
|
643
|
+
disarm(turn);
|
|
644
|
+
closeBrainCards(sessionId);
|
|
645
|
+
// The owner wrote something else while this turn waited for them (moveOn, or a request
|
|
646
|
+
// that came after their message): their message is next. The brain was told to end with
|
|
647
|
+
// no text unless it has a result (MOVED_ON_NOTE), so what it still says is forwarded as
|
|
648
|
+
// it is, whatever its length. Only a turn that ends with nothing to say closes quietly:
|
|
649
|
+
// no "Done.", no "Stopped.".
|
|
650
|
+
const movedOn = turn.superseded === true && (outcome === 'done' || outcome === 'stopped') && !finalText(turn) && !turn.resultError;
|
|
651
|
+
if (movedOn && (turn.kind === 'wa' || turn.kind === 'followup')) {
|
|
652
|
+
if (turn.kind === 'followup') state.followUp = null;
|
|
653
|
+
else { presence('paused'); state.inflight = null; persistBridgeState(); react(turn.sourceIds, 'done'); }
|
|
654
|
+
} else if (turn.kind === 'followup') {
|
|
655
|
+
// The task the phone decided on a card: its answer is a reply, not an update.
|
|
656
|
+
state.followUp = null;
|
|
657
|
+
const text = finalText(turn);
|
|
658
|
+
if (outcome === 'failed') await say(`Something went wrong: ${String(message || turn.resultError || 'the turn failed').slice(0, 400)}`, { maxChunks: 1 });
|
|
659
|
+
else if (text && outcome !== 'stopped') await say(text, { markdown: true });
|
|
660
|
+
} else if (turn.kind === 'wa') {
|
|
661
|
+
presence('paused');
|
|
662
|
+
state.inflight = null;
|
|
663
|
+
persistBridgeState();
|
|
664
|
+
const text = finalText(turn);
|
|
665
|
+
if (outcome === 'watchdog') {
|
|
666
|
+
if (text) await say(text, { markdown: true, replyTo: turn.replyTo });
|
|
667
|
+
await say('This is taking longer than 30 minutes, so I stopped waiting here. Check SynaBun on your computer for the result.', { maxChunks: 1 });
|
|
668
|
+
react(turn.sourceIds, 'failed');
|
|
669
|
+
} else if (outcome === 'failed') {
|
|
670
|
+
if (text) await say(text, { markdown: true, replyTo: turn.replyTo });
|
|
671
|
+
await say(`Something went wrong: ${String(message || turn.resultError || 'the turn failed').slice(0, 400)}`, { maxChunks: 1 });
|
|
672
|
+
react(turn.sourceIds, 'failed');
|
|
673
|
+
} else if (outcome === 'stopped') {
|
|
674
|
+
// Stopped at the Mac: its one line was already said (onComputer); what the Assistant had to say still goes.
|
|
675
|
+
if (turn.stoppedOnMac) { if (text) await say(text, { markdown: true, replyTo: turn.replyTo }); }
|
|
676
|
+
else if (!turn.stoppedByPhone) await say(text ? `${text}\n\n(Stopped.)` : 'Stopped.', { markdown: !!text, replyTo: turn.replyTo });
|
|
677
|
+
react(turn.sourceIds, 'stopped');
|
|
678
|
+
} else {
|
|
679
|
+
if (text) await say(text, { markdown: true, replyTo: turn.replyTo });
|
|
680
|
+
else if (turn.resultError) await say(`The Assistant stopped (${turn.resultError}). Check SynaBun on your computer.`, { maxChunks: 1, replyTo: turn.replyTo });
|
|
681
|
+
else if (!hasOpenCards(sessionId)) await say('Done.', { maxChunks: 1, replyTo: turn.replyTo });
|
|
682
|
+
react(turn.sourceIds, turn.resultError ? 'failed' : 'done');
|
|
683
|
+
}
|
|
684
|
+
} else if ((turn.kind === 'mailbox' || turn.kind === 'background') && outcome === 'done') {
|
|
685
|
+
forwardUpdate(turn);
|
|
686
|
+
}
|
|
687
|
+
if (sessionId === state.sessionId) scheduleDrain(L.drainMs);
|
|
688
|
+
}
|
|
689
|
+
function hasOpenCards(sessionId) { for (const card of state.cards.values()) if (card.sessionId === sessionId) return true; return false; }
|
|
690
|
+
|
|
691
|
+
// ── forwarding results ─────────────────────────────────────────────────────
|
|
692
|
+
function forwardUpdate(turn) {
|
|
693
|
+
const cfg = readConfig();
|
|
694
|
+
if (cfg.forwardBackground === false || turn.memoryOnly) return;
|
|
695
|
+
const text = finalText(turn);
|
|
696
|
+
if (!text) return;
|
|
697
|
+
state.forwards = state.forwards.filter((at) => now() - at < HOUR);
|
|
698
|
+
if (state.forwards.length >= L.forwardPerHour) {
|
|
699
|
+
state.forwardSuppressed += 1;
|
|
700
|
+
if (!state.forwardSummaryTimer) {
|
|
701
|
+
const wait = Math.max(1000, HOUR - (now() - state.forwards[0]));
|
|
702
|
+
state.forwardSummaryTimer = T.setTimeout(() => {
|
|
703
|
+
state.forwardSummaryTimer = null;
|
|
704
|
+
const n = state.forwardSuppressed;
|
|
705
|
+
state.forwardSuppressed = 0;
|
|
706
|
+
if (n > 0) say(`${n} more update${n === 1 ? '' : 's'} in SynaBun.`, { maxChunks: 1 });
|
|
707
|
+
}, wait);
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
state.forwards.push(now());
|
|
712
|
+
const previous = turn.sessionId !== state.sessionId ? ' (earlier conversation)' : '';
|
|
713
|
+
say(text, { markdown: true, lead: `*Update*${previous}` });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ── runtime packets ────────────────────────────────────────────────────────
|
|
717
|
+
function onRuntimeEvent(message) {
|
|
718
|
+
if (message?.type !== 'assistant:session-ended') return;
|
|
719
|
+
const id = message.session?.id;
|
|
720
|
+
if (id && id === state.sessionId) { state.sessionId = null; log('whatsapp:bridge', `session ${id} ended; the next message starts a new one`); }
|
|
721
|
+
}
|
|
722
|
+
function onPacket({ sessionId, packet } = {}) {
|
|
723
|
+
if (state.stopped || !packet || !ours(sessionId)) return;
|
|
724
|
+
const type = packet.type;
|
|
725
|
+
const live = state.turns.get(sessionId);
|
|
726
|
+
if (live) live.lastPacketAt = now();
|
|
727
|
+
if (type === 'event') { onEvent(sessionId, packet.event || {}, packet); return; }
|
|
728
|
+
if (type === 'control_request') { onCard(sessionId, packet); return; }
|
|
729
|
+
if (type === 'control_cancelled') { onCardClosed(sessionId, String(packet.request_id ?? ''), packet.reason === 'timeout' ? 'timeout' : 'cancelled'); return; }
|
|
730
|
+
if (type === 'control_resolved') { onCardClosed(sessionId, String(packet.request_id ?? ''), packet.origin === 'whatsapp' ? 'answered' : 'desktop'); return; }
|
|
731
|
+
if (type === 'turn_started') { if (!state.turns.has(sessionId)) makeTurn(sessionId, 'background'); return; }
|
|
732
|
+
if (type === 'done') { finalizeTurn(sessionId, 'done'); return; }
|
|
733
|
+
if (type === 'aborted') { finalizeTurn(sessionId, 'stopped'); return; }
|
|
734
|
+
if (type === 'error') {
|
|
735
|
+
// An error ends a turn only when nothing runs any more (a refused mailbox
|
|
736
|
+
// turn, a failed start); a brain still busy carries on. Checked again shortly.
|
|
737
|
+
const turn = state.turns.get(sessionId);
|
|
738
|
+
if (!turn) return;
|
|
739
|
+
const check = (left) => {
|
|
740
|
+
if (state.turns.get(sessionId) !== turn || turn.state === 'SUBMITTING') return;
|
|
741
|
+
let busy = false;
|
|
742
|
+
try { busy = !!runtime()?.isBusy?.(sessionId); } catch { busy = false; }
|
|
743
|
+
if (!busy) { finalizeTurn(sessionId, 'failed', { message: packet.message || null }); return; }
|
|
744
|
+
if (left > 0) arm(turn, 1000, () => check(left - 1));
|
|
745
|
+
};
|
|
746
|
+
check(5);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (type === 'assistant:dispatch') { onDispatch(sessionId, packet); return; }
|
|
750
|
+
if (type === 'assistant:computer') { onComputer(sessionId, packet); }
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Computer control of this conversation was stopped at the Mac: the phone
|
|
754
|
+
* hears it once, in one line (the turn that was stopped adds no "Stopped.").
|
|
755
|
+
* A turn typed on the desktop is the desktop's: the person is at the Mac.
|
|
756
|
+
*/
|
|
757
|
+
function onComputer(sessionId, packet) {
|
|
758
|
+
if (packet.phase !== 'stopped' || packet.by !== 'mac') return;
|
|
759
|
+
const turn = state.turns.get(sessionId);
|
|
760
|
+
if (turn?.kind === 'external' || turn?.stoppedOnMac) return;
|
|
761
|
+
if (turn) turn.stoppedOnMac = true;
|
|
762
|
+
say(MAC_STOP_TEXT, { maxChunks: 1 });
|
|
763
|
+
}
|
|
764
|
+
function onEvent(sessionId, ev, packet) {
|
|
765
|
+
switch (ev.type) {
|
|
766
|
+
case 'synabun.user_prompt':
|
|
767
|
+
// Our own prompt (the WhatsApp turn exists already); any other is someone else's turn.
|
|
768
|
+
if (ev.origin !== 'whatsapp' && !state.turns.has(sessionId)) makeTurn(sessionId, 'external');
|
|
769
|
+
return;
|
|
770
|
+
case 'synabun.mailbox': {
|
|
771
|
+
const items = Array.isArray(ev.items) ? ev.items : [];
|
|
772
|
+
const turn = state.turns.get(sessionId);
|
|
773
|
+
const memoryOnly = items.length > 0 && items.every((item) => item?.kind === 'memory_due');
|
|
774
|
+
const kind = followUpFor(sessionId) && !memoryOnly ? 'followup' : 'mailbox';
|
|
775
|
+
if (!turn) makeTurn(sessionId, kind, { memoryOnly });
|
|
776
|
+
else if (turn.kind !== 'wa' && turn.kind !== 'followup') { turn.kind = kind; turn.memoryOnly = memoryOnly; }
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
case 'assistant': {
|
|
780
|
+
if (ev.parent_tool_use_id) return;
|
|
781
|
+
const turn = ensureTurn(sessionId, 'external');
|
|
782
|
+
const key = assistantEventKey(ev);
|
|
783
|
+
if (turn.seen.has(key)) return;
|
|
784
|
+
turn.seen.add(key);
|
|
785
|
+
addSegment(turn, ev);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
case 'result': {
|
|
789
|
+
if (ev.parent_tool_use_id) return;
|
|
790
|
+
const turn = ensureTurn(sessionId, 'external');
|
|
791
|
+
if (typeof ev.result === 'string' && ev.result.trim()) turn.resultText = ev.result;
|
|
792
|
+
if (ev.is_error === true || /^error/.test(String(ev.subtype || ''))) turn.resultError = String(ev.subtype || 'error');
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
case 'synabun.dispatch_control_request':
|
|
796
|
+
onCard(sessionId, packet);
|
|
797
|
+
return;
|
|
798
|
+
case 'synabun.dispatch_control_resolved':
|
|
799
|
+
onCardClosed(sessionId, String(ev.request_id ?? ''), ev.resolvedBy === 'whatsapp' ? 'answered' : 'desktop', { runId: ev.runId });
|
|
800
|
+
return;
|
|
801
|
+
case 'synabun.route': {
|
|
802
|
+
const routeId = ev.route?.routeId;
|
|
803
|
+
if (!routeId || !ROUTE_DONE_PHASES.has(ev.phase)) return;
|
|
804
|
+
for (const card of [...state.cards.values()]) {
|
|
805
|
+
if (card.sessionId === sessionId && card.route?.routeId === routeId && !card.answering) closeCard(card, ev.phase === 'expired' ? 'expired' : 'desktop');
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
case 'synabun.clarify': {
|
|
810
|
+
const requestId = ev.brief?.round?.requestId ? String(ev.brief.round.requestId) : '';
|
|
811
|
+
if (!requestId || !CLARIFY_DONE_PHASES.has(ev.phase)) return;
|
|
812
|
+
// 'chat': a prompt (the phone's own, or the desktop's) settled the questions; nothing was answered elsewhere.
|
|
813
|
+
onCardClosed(sessionId, requestId, ev.phase === 'chat' || ev.phase === 'cancelled' ? 'moved_on' : 'desktop');
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
default:
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function onDispatch(sessionId, packet) {
|
|
820
|
+
if (packet.reason !== 'started' || !packet.run) return;
|
|
821
|
+
const cfg = readConfig();
|
|
822
|
+
if (cfg.progress === 'off' || state.turns.get(sessionId)?.kind === 'external') return;
|
|
823
|
+
const run = packet.run;
|
|
824
|
+
state.dispatchBatch.push(`Started agent: ${String(run.title || run.task || 'a task').slice(0, 80)} (${run.provider || 'agent'}${run.model ? `/${run.model}` : ''})`);
|
|
825
|
+
if (state.dispatchTimer) return;
|
|
826
|
+
state.dispatchTimer = T.setTimeout(() => {
|
|
827
|
+
state.dispatchTimer = null;
|
|
828
|
+
const lines = state.dispatchBatch.splice(0, state.dispatchBatch.length);
|
|
829
|
+
if (lines.length) say(lines.join('\n'), { maxChunks: 1 });
|
|
830
|
+
}, L.dispatchBatchMs);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ── cards ──────────────────────────────────────────────────────────────────
|
|
834
|
+
function onCard(sessionId, packet) {
|
|
835
|
+
// A desktop turn's cards are the desktop's, except the request to control the Mac: in a
|
|
836
|
+
// WhatsApp conversation only the owner's phone can grant that, whoever started the turn.
|
|
837
|
+
const turn = state.turns.get(sessionId);
|
|
838
|
+
const card = cardFromPacket(packet, { sessionId, level: levelOf(sessionId), now: now() });
|
|
839
|
+
if (turn?.kind === 'external' && card?.kind !== 'computer') return;
|
|
840
|
+
if (!card || state.cards.has(card.key) || state.closedCards.has(card.key)) return;
|
|
841
|
+
// The owner already wrote something newer than this request: they moved on from the turn
|
|
842
|
+
// that asks (moveOn), or a message of theirs waits for it to end (queued, or still inside
|
|
843
|
+
// the coalescer window). The request closes at once without a grant, unseen: an approval is
|
|
844
|
+
// denied, a question is cancelled (text written before it existed is not its answer), and
|
|
845
|
+
// the waiting message is the next prompt. A worker's own request is still theirs to
|
|
846
|
+
// answer: it holds no turn, so no message waits behind it. Only the conversation the
|
|
847
|
+
// waiting message goes to counts: an earlier conversation's request holds nothing.
|
|
848
|
+
if (card.source !== 'dispatch' && (turn?.superseded || (sessionId === state.sessionId && messageWaiting()))) {
|
|
849
|
+
if (turn) turn.superseded = true;
|
|
850
|
+
state.closedCards.set(card.key, { at: now(), by: 'moved_on' });
|
|
851
|
+
failClose(card).catch(() => {});
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
state.cards.set(card.key, card);
|
|
855
|
+
if (!state.currentKey) showNextCard();
|
|
856
|
+
}
|
|
857
|
+
/** A phone message is on its way to the Assistant: inside the coalescer window, in the queue, or being started. */
|
|
858
|
+
function messageWaiting() {
|
|
859
|
+
if (state.queue.length > 0 || state.starting) return true;
|
|
860
|
+
let held = null;
|
|
861
|
+
try { held = coalescer?.size?.(); } catch { held = null; }
|
|
862
|
+
return (Number.isFinite(held) ? held : state.buffered) > 0;
|
|
863
|
+
}
|
|
864
|
+
function currentCard() { return state.currentKey ? state.cards.get(state.currentKey) || null : null; }
|
|
865
|
+
function showNextCard({ replaced = false } = {}) {
|
|
866
|
+
if (state.currentKey && state.cards.has(state.currentKey)) return;
|
|
867
|
+
state.currentKey = null;
|
|
868
|
+
const next = state.cards.values().next().value;
|
|
869
|
+
if (!next) return;
|
|
870
|
+
state.currentKey = next.key;
|
|
871
|
+
// Several open requests, or one right after another was answered elsewhere: a code says which one a reply means.
|
|
872
|
+
if (!next.tag && (state.cards.size > 1 || (replaced && now() - state.lastDesktopAnswerAt < MINUTE))) next.tag = randomTag(rng);
|
|
873
|
+
sendCard(next);
|
|
874
|
+
}
|
|
875
|
+
async function sendCard(card) {
|
|
876
|
+
const text = renderCard(card, { formatPlan: (plan) => { try { return format?.toWhatsApp ? format.toWhatsApp(plan) : plan; } catch { return plan; } } });
|
|
877
|
+
const ids = await say(text, { maxChunks: card.kind === 'plan' ? null : 1, cardKey: card.key });
|
|
878
|
+
for (const id of ids) mapCardMessage(card.key, id);
|
|
879
|
+
}
|
|
880
|
+
/** A WhatsApp message id is a request's message: a quote of it names that request (open, or closed by now). */
|
|
881
|
+
function mapCardMessage(cardKey, id) {
|
|
882
|
+
state.cardByMessage.set(id, cardKey);
|
|
883
|
+
const card = state.cards.get(cardKey);
|
|
884
|
+
if (card && !card.messageIds.includes(id)) card.messageIds.push(id);
|
|
885
|
+
}
|
|
886
|
+
function findCard(sessionId, requestId, runId = null) {
|
|
887
|
+
for (const card of state.cards.values()) {
|
|
888
|
+
if (card.sessionId === sessionId && card.requestId === requestId && (!runId || card.runId === runId)) return card;
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
function onCardClosed(sessionId, requestId, reason, { runId = null } = {}) {
|
|
893
|
+
const card = findCard(sessionId, requestId, runId);
|
|
894
|
+
if (!card || card.answering) return;
|
|
895
|
+
if (reason === 'desktop') state.lastDesktopAnswerAt = now();
|
|
896
|
+
closeCard(card, reason);
|
|
897
|
+
}
|
|
898
|
+
function closeCard(card, reason, { quiet = false, next = true } = {}) {
|
|
899
|
+
state.closedCards.set(card.key, { at: now(), by: reason });
|
|
900
|
+
if (state.closedCards.size > 100) state.closedCards.delete(state.closedCards.keys().next().value);
|
|
901
|
+
if (!state.cards.delete(card.key)) return;
|
|
902
|
+
const wasCurrent = state.currentKey === card.key;
|
|
903
|
+
if (wasCurrent) state.currentKey = null;
|
|
904
|
+
// A route or clarify card the phone saw was decided (here or on the desktop): what follows answers the phone.
|
|
905
|
+
if ((card.kind === 'route' || card.kind === 'clarify') && (reason === 'answered' || reason === 'desktop')) {
|
|
906
|
+
state.followUp = { sessionId: card.sessionId, at: now() };
|
|
907
|
+
}
|
|
908
|
+
if (!quiet && reason === 'timeout' && wasCurrent) {
|
|
909
|
+
const minutes = Math.max(1, Math.round((now() - card.createdAt) / MINUTE));
|
|
910
|
+
say(`No answer in ${minutes} min — declined.`, { maxChunks: 1 });
|
|
911
|
+
} else if (!quiet && reason === 'expired' && wasCurrent) {
|
|
912
|
+
say('That choice expired without an answer.', { maxChunks: 1 });
|
|
913
|
+
}
|
|
914
|
+
if (wasCurrent && next) showNextCard({ replaced: reason === 'desktop' });
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Close a request without granting anything, because the owner wrote
|
|
918
|
+
* something else: a denial (permission, question, plan), a cancelled route or
|
|
919
|
+
* clarify round. Through the same paths as an answer, with origin 'whatsapp',
|
|
920
|
+
* so the desktop card locks as resolved. Never an approval.
|
|
921
|
+
*/
|
|
922
|
+
async function failClose(card) {
|
|
923
|
+
const rt = runtime();
|
|
924
|
+
card.answering = true;
|
|
925
|
+
try {
|
|
926
|
+
// `superseded` travels as the caller's option: the runtime never takes it from a response.
|
|
927
|
+
const { superseded: _superseded, ...response } = failClosedResponse(card, card.source === 'dispatch' ? WORKER_MOVED_ON_NOTE : undefined) || {};
|
|
928
|
+
if (rt && card.source === 'dispatch') rt.answerDispatchControl(card.sessionId, card.runId, card.requestId, response, { origin: 'whatsapp' });
|
|
929
|
+
else if (rt) await rt.answerControl(card.sessionId, card.requestId, response, { origin: 'whatsapp', superseded: true });
|
|
930
|
+
} catch (error) {
|
|
931
|
+
log('whatsapp:bridge', `closing ${card.requestId}: ${error?.message || error}`);
|
|
932
|
+
} finally { card.answering = false; }
|
|
933
|
+
closeCard(card, 'moved_on', { quiet: true, next: false });
|
|
934
|
+
}
|
|
935
|
+
/** The open requests of a session the bridge is not showing (a desktop turn's, or raised before this bridge started). */
|
|
936
|
+
function untrackedPending(sessionId) {
|
|
937
|
+
const rt = runtime();
|
|
938
|
+
if (!rt?.pendingControls || !sessionId) return [];
|
|
939
|
+
let packets = [];
|
|
940
|
+
try { packets = rt.pendingControls(sessionId) || []; } catch { packets = []; }
|
|
941
|
+
const out = [];
|
|
942
|
+
for (const packet of packets) {
|
|
943
|
+
const card = cardFromPacket(packet, { sessionId, level: levelOf(sessionId), now: now() });
|
|
944
|
+
if (card && !state.cards.has(card.key)) out.push(card);
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* The owner wrote something that answers nothing that waits: every open
|
|
950
|
+
* request closes without a grant and the turn that asked is marked, so the
|
|
951
|
+
* message that follows is never held behind it. Nothing is approved here.
|
|
952
|
+
*/
|
|
953
|
+
async function moveOn() {
|
|
954
|
+
// Only the conversation the next prompt goes to: a late request of an earlier conversation
|
|
955
|
+
// (a worker of it) is not this message's to close; it stays open and is shown when its turn comes.
|
|
956
|
+
const open = [...[...state.cards.values()].filter((card) => card.sessionId === state.sessionId), ...untrackedPending(state.sessionId)];
|
|
957
|
+
const closed = await closeWithoutGrant(open);
|
|
958
|
+
if (!state.currentKey) showNextCard();
|
|
959
|
+
return closed;
|
|
960
|
+
}
|
|
961
|
+
async function closeWithoutGrant(open) {
|
|
962
|
+
// Marked first: what the turn asks while these close (the brain trying something else) closes the same way (onCard).
|
|
963
|
+
for (const card of open) {
|
|
964
|
+
if (card.source === 'dispatch') continue;
|
|
965
|
+
const turn = state.turns.get(card.sessionId);
|
|
966
|
+
if (turn && turn.kind !== 'external') turn.superseded = true;
|
|
967
|
+
}
|
|
968
|
+
for (const card of open) await failClose(card);
|
|
969
|
+
return open.length;
|
|
970
|
+
}
|
|
971
|
+
/** Open requests that hold the conversation the next prompt goes to (a worker's own request holds no turn). */
|
|
972
|
+
function holdingCards() {
|
|
973
|
+
return [...state.cards.values()].filter((card) => card.source !== 'dispatch' && card.sessionId === state.sessionId);
|
|
974
|
+
}
|
|
975
|
+
/** → true when the message was used up as an answer; false when it goes on as a prompt. */
|
|
976
|
+
async function answer(card, result, message) {
|
|
977
|
+
const rt = runtime();
|
|
978
|
+
if (!rt) { await say('The Assistant is not available right now.', { maxChunks: 1 }); return true; }
|
|
979
|
+
card.answering = true;
|
|
980
|
+
let outcome;
|
|
981
|
+
try {
|
|
982
|
+
outcome = card.source === 'dispatch'
|
|
983
|
+
? rt.answerDispatchControl(card.sessionId, card.runId, card.requestId, result.response, { origin: 'whatsapp' })
|
|
984
|
+
: await rt.answerControl(card.sessionId, card.requestId, result.response, { origin: 'whatsapp', authority: phoneAuthority });
|
|
985
|
+
} catch (error) {
|
|
986
|
+
outcome = { ok: false, code: error?.code || 'ANSWER_FAILED', message: error?.message || String(error) };
|
|
987
|
+
} finally {
|
|
988
|
+
card.answering = false;
|
|
989
|
+
}
|
|
990
|
+
if (outcome?.ok) {
|
|
991
|
+
react([message?.id].filter(Boolean), 'done');
|
|
992
|
+
closeCard(card, 'answered', { quiet: true });
|
|
993
|
+
const ack = ackText(card, result);
|
|
994
|
+
if (ack) await say(ack, { maxChunks: 1 });
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
if (ALREADY_ANSWERED.has(outcome?.code)) {
|
|
998
|
+
// Someone else answered it while this answer was on its way (the desktop won the race for
|
|
999
|
+
// the same request). A bare answer gets the note. A message that says more ("use Opus",
|
|
1000
|
+
// "no, use staging") is the owner's next message: what is still open closes without a
|
|
1001
|
+
// grant and it goes on as a prompt. Nothing is granted on this path.
|
|
1002
|
+
const bare = isBareAnswer(message?.text);
|
|
1003
|
+
closeCard(card, 'desktop', { quiet: true, next: bare });
|
|
1004
|
+
if (bare) { await say('Already answered in SynaBun.', { maxChunks: 1 }); return true; }
|
|
1005
|
+
await moveOn();
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
await say(`That answer did not go through: ${String(outcome?.message || outcome?.code || 'unknown error').slice(0, 300)}`, { maxChunks: 1 });
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
/** One short line after an approval was answered. An answered question needs none: the Assistant carries on. */
|
|
1012
|
+
function ackText(card, result) {
|
|
1013
|
+
const summary = String(result.summary || '');
|
|
1014
|
+
if (card.kind === 'route') return summary === 'cancelled' ? "OK, I won't start that." : `OK, ${summary}.`;
|
|
1015
|
+
if (summary === 'computer allowed') return 'OK, I will use your Mac for this task. Esc on the Mac stops me.';
|
|
1016
|
+
if (summary === 'allowed once') return 'OK, going ahead.';
|
|
1017
|
+
if (summary === 'denied') return "OK, I won't.";
|
|
1018
|
+
if (summary === 'approved') return 'OK, going ahead with the plan.';
|
|
1019
|
+
if (summary === 'keep planning') return "OK, I'll keep planning.";
|
|
1020
|
+
if (summary === 'skipped' || summary === 'declined') return 'OK, skipping that.';
|
|
1021
|
+
return '';
|
|
1022
|
+
}
|
|
1023
|
+
/** The desktop answered a request the phone saw, and nothing has been said here since. */
|
|
1024
|
+
function recentlyAnsweredElsewhere() {
|
|
1025
|
+
return state.lastDesktopAnswerAt && now() - state.lastDesktopAnswerAt < L.desktopAnsweredMs && !(state.lastReplyAt > state.lastDesktopAnswerAt);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ── commands ───────────────────────────────────────────────────────────────
|
|
1029
|
+
async function runCommand(command, message) {
|
|
1030
|
+
switch (command.name) {
|
|
1031
|
+
case 'help': await say(HELP_TEXT, { maxChunks: 2 }); return;
|
|
1032
|
+
case 'status': await say(statusText(statusSnapshot()), { maxChunks: 1 }); return;
|
|
1033
|
+
case 'stop': {
|
|
1034
|
+
const { stoppedRuns, clearedQueue } = await stopEverything({ reason: 'phone' });
|
|
1035
|
+
const n = stoppedRuns + clearedQueue;
|
|
1036
|
+
await say(`Stopped. Cancelled ${n} task${n === 1 ? '' : 's'}.`, { maxChunks: 1 });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
case 'new':
|
|
1040
|
+
if (command.args) { await say('/new takes no arguments: send /new on its own, then your message.', { maxChunks: 1 }); return; }
|
|
1041
|
+
try { await newSession('command'); await say('New conversation started. Your next message goes to it.', { maxChunks: 1 }); }
|
|
1042
|
+
catch (error) { await say(`Could not start a new conversation: ${error?.message || error}`, { maxChunks: 1 }); }
|
|
1043
|
+
return;
|
|
1044
|
+
case 'pause':
|
|
1045
|
+
pause('phone');
|
|
1046
|
+
await say('Paused. I will not act on your messages until you send /resume.', { maxChunks: 1 });
|
|
1047
|
+
return;
|
|
1048
|
+
case 'resume': {
|
|
1049
|
+
if (!state.paused) { await say('Not paused.', { maxChunks: 1 }); return; }
|
|
1050
|
+
if (state.pausedBy && state.pausedBy !== 'phone') { await say('This was paused from SynaBun on your computer: resume it there.', { maxChunks: 1 }); return; }
|
|
1051
|
+
resume('phone');
|
|
1052
|
+
await say('Resumed. Send me what you need.', { maxChunks: 1 });
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
case 'cards': {
|
|
1056
|
+
if (!state.cards.size) { await say('Nothing is waiting for you.', { maxChunks: 1 }); return; }
|
|
1057
|
+
const card = currentCard();
|
|
1058
|
+
if (card) await sendCard(card);
|
|
1059
|
+
else showNextCard();
|
|
1060
|
+
if (state.cards.size > 1) await say(`${state.cards.size - 1} more after this one.`, { maxChunks: 1 });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
default:
|
|
1064
|
+
void message;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function statusSnapshot() {
|
|
1068
|
+
const view = state.sessionId ? sessionView(state.sessionId) : null;
|
|
1069
|
+
const turn = state.sessionId ? state.turns.get(state.sessionId) : null;
|
|
1070
|
+
const rt = runtime();
|
|
1071
|
+
let brainCapUsd = null;
|
|
1072
|
+
try { brainCapUsd = rt?.brainCapUsd?.() ?? null; } catch { brainCapUsd = null; }
|
|
1073
|
+
const cfg = readConfig();
|
|
1074
|
+
const current = currentCard();
|
|
1075
|
+
return {
|
|
1076
|
+
connected: isConnected(), sessionId: state.sessionId, title: view?.title || null, brain: view?.brain || null,
|
|
1077
|
+
running: !!turn || !!view?.running, turnKind: turn?.kind || null, queued: state.queue.length,
|
|
1078
|
+
pendingCards: state.cards.size, currentCard: current ? (current.kind === 'route' ? 'where to run a task' : current.kind === 'computer' ? 'controlling your Mac' : current.kind) : null,
|
|
1079
|
+
budget: view?.budget || null, brainUsd: view?.costUsd ?? null, brainCapUsd,
|
|
1080
|
+
level: state.sessionId ? levelOf(state.sessionId) : cfg.level, paused: state.paused, pausedBy: state.pausedBy,
|
|
1081
|
+
autonomousUntil: cfg.autonomousUntil || null,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
/** /stop and stopAll: the turn, the session's runs, open cards and the queue. */
|
|
1085
|
+
async function stopEverything({ reason = 'phone' } = {}) {
|
|
1086
|
+
const rt = runtime();
|
|
1087
|
+
const id = state.sessionId;
|
|
1088
|
+
let stoppedRuns = 0;
|
|
1089
|
+
const turn = id ? state.turns.get(id) : null;
|
|
1090
|
+
if (turn) turn.stoppedByPhone = reason === 'phone';
|
|
1091
|
+
if (rt && id) { try { await rt.stopTurn?.(id, { origin: 'whatsapp' }); } catch {} }
|
|
1092
|
+
const d = dispatcher();
|
|
1093
|
+
if (d?.list && d?.stop && id) {
|
|
1094
|
+
let runs = [];
|
|
1095
|
+
try { runs = d.list({ assistantSessionId: id, activeOnly: true }) || []; } catch { runs = []; }
|
|
1096
|
+
for (const run of runs) { try { await d.stop(run.runId, 'user'); stoppedRuns += 1; } catch {} }
|
|
1097
|
+
}
|
|
1098
|
+
for (const card of [...state.cards.values()]) {
|
|
1099
|
+
if (card.sessionId !== id) continue;
|
|
1100
|
+
const denied = answerCard(card, { type: 'cancel', rest: '' }, { quoted: true });
|
|
1101
|
+
if (denied.action === 'respond' && rt) {
|
|
1102
|
+
card.answering = true;
|
|
1103
|
+
try {
|
|
1104
|
+
if (card.source === 'dispatch') rt.answerDispatchControl(card.sessionId, card.runId, card.requestId, denied.response, { origin: 'whatsapp' });
|
|
1105
|
+
else await rt.answerControl(card.sessionId, card.requestId, denied.response, { origin: 'whatsapp' });
|
|
1106
|
+
} catch {} finally { card.answering = false; }
|
|
1107
|
+
}
|
|
1108
|
+
closeCard(card, 'stopped', { quiet: true });
|
|
1109
|
+
}
|
|
1110
|
+
const cleared = state.queue.splice(0, state.queue.length);
|
|
1111
|
+
for (const job of cleared) react(job.sourceIds, 'stopped');
|
|
1112
|
+
persistBridgeState();
|
|
1113
|
+
return { stoppedRuns, clearedQueue: cleared.length };
|
|
1114
|
+
}
|
|
1115
|
+
function pause(by = 'desktop') {
|
|
1116
|
+
state.paused = true;
|
|
1117
|
+
state.pausedBy = by === 'phone' ? 'phone' : 'desktop';
|
|
1118
|
+
state.pausedNoticeSent = false;
|
|
1119
|
+
writeConfig({ paused: true, pausedBy: state.pausedBy });
|
|
1120
|
+
state.policySeen.clear();
|
|
1121
|
+
syncPolicies();
|
|
1122
|
+
return status();
|
|
1123
|
+
}
|
|
1124
|
+
function resume(by = 'desktop') {
|
|
1125
|
+
if (!state.paused) return status();
|
|
1126
|
+
if (by === 'phone' && state.pausedBy && state.pausedBy !== 'phone') return status();
|
|
1127
|
+
state.paused = false;
|
|
1128
|
+
state.pausedBy = null;
|
|
1129
|
+
writeConfig({ paused: false, pausedBy: null });
|
|
1130
|
+
state.policySeen.clear();
|
|
1131
|
+
syncPolicies();
|
|
1132
|
+
scheduleDrain(L.drainMs);
|
|
1133
|
+
return status();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ── inbound ────────────────────────────────────────────────────────────────
|
|
1137
|
+
function rateLimited() {
|
|
1138
|
+
const t = now();
|
|
1139
|
+
if (t - state.minuteWindow.start >= MINUTE) state.minuteWindow = { start: t, count: 0, noticed: false };
|
|
1140
|
+
if (t - state.dayWindow.start >= 24 * HOUR) state.dayWindow = { start: t, count: 0, noticed: false };
|
|
1141
|
+
state.minuteWindow.count += 1;
|
|
1142
|
+
state.dayWindow.count += 1;
|
|
1143
|
+
if (state.dayWindow.count > L.perDay) {
|
|
1144
|
+
if (!state.dayWindow.noticed) { state.dayWindow.noticed = true; say(`That is ${L.perDay} messages today, my daily limit. I will pick up again tomorrow.`, { maxChunks: 1 }); }
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
if (state.minuteWindow.count > L.perMinute) {
|
|
1148
|
+
if (!state.minuteWindow.noticed) { state.minuteWindow.noticed = true; say('Too many messages at once: I will read new ones again in a minute.', { maxChunks: 1 }); }
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
function noteStale() {
|
|
1154
|
+
state.staleSkipped += 1;
|
|
1155
|
+
if (state.staleTimer) T.clearTimeout(state.staleTimer);
|
|
1156
|
+
state.staleTimer = T.setTimeout(() => {
|
|
1157
|
+
state.staleTimer = null;
|
|
1158
|
+
const n = state.staleSkipped;
|
|
1159
|
+
state.staleSkipped = 0;
|
|
1160
|
+
if (n > 0) say(`I was offline and skipped ${n} older message${n === 1 ? '' : 's'}; resend what you still need.`, { maxChunks: 1 });
|
|
1161
|
+
}, L.staleNoticeMs);
|
|
1162
|
+
}
|
|
1163
|
+
/** One owner message from the transport (InboundMessage; never a number or a JID). */
|
|
1164
|
+
async function onInbound(message) {
|
|
1165
|
+
if (state.stopped || !message || message.owner !== true) return;
|
|
1166
|
+
if (message.id) { try { const r = transport.markRead?.([message.id]); if (r?.catch) r.catch(() => {}); } catch {} }
|
|
1167
|
+
// Sent longer ago than staleMs (WhatsApp held it while SynaBun was offline:
|
|
1168
|
+
// `backlog`): never acted on, only counted for one note. Checked first, so a
|
|
1169
|
+
// backlog neither uses up the rate limits nor counts as activity.
|
|
1170
|
+
if (now() - tsMs(message.ts, now()) > L.staleMs) { noteStale(); return; }
|
|
1171
|
+
if (rateLimited()) return;
|
|
1172
|
+
const activityBefore = state.lastActivityAt;
|
|
1173
|
+
state.lastActivityAt = now();
|
|
1174
|
+
state.lastInboundId = message.id || state.lastInboundId;
|
|
1175
|
+
runtime();
|
|
1176
|
+
syncPolicies();
|
|
1177
|
+
const text = String(message.text ?? '');
|
|
1178
|
+
const images = Array.isArray(message.images) ? message.images : [];
|
|
1179
|
+
if (message.unsupported && !text.trim() && !images.length) {
|
|
1180
|
+
const type = String(message.unsupported.type || 'this kind of');
|
|
1181
|
+
const last = state.unsupportedAt.get(type) || 0;
|
|
1182
|
+
if (now() - last >= L.unsupportedEveryMs) {
|
|
1183
|
+
state.unsupportedAt.set(type, now());
|
|
1184
|
+
await say(`I can read text and pictures for now; ${type} messages are not supported yet.`, { maxChunks: 1 });
|
|
1185
|
+
}
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const command = parseCommand(message);
|
|
1189
|
+
if (command?.kind === 'command') { await runCommand(command, message); return; }
|
|
1190
|
+
if (command?.kind === 'refused') { await say(SETTINGS_REFUSAL, { maxChunks: 1 }); return; }
|
|
1191
|
+
const promptMessage = command?.kind === 'literal' ? { ...message, text: command.text } : message;
|
|
1192
|
+
if (state.paused) {
|
|
1193
|
+
if (!state.pausedNoticeSent) {
|
|
1194
|
+
state.pausedNoticeSent = true;
|
|
1195
|
+
await say(state.pausedBy === 'desktop' ? 'SynaBun is paused from your computer: resume it there to continue.' : 'Paused: send /resume to continue.', { maxChunks: 1 });
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
// Something is waiting for the owner: a clear answer answers it; anything else closes it
|
|
1200
|
+
// without a grant and carries on as the next prompt. Only words the owner typed can answer:
|
|
1201
|
+
// never a forwarded message, a picture or "//text".
|
|
1202
|
+
const answerable = !images.length && command?.kind !== 'literal' && message.forwarded !== true;
|
|
1203
|
+
if (await replyToPending(message, { answerable })) return;
|
|
1204
|
+
if (rotationDue(activityBefore)) state.rotateOnNextSubmit = true;
|
|
1205
|
+
// A request raised between that look and here is older than this message too (onCard only sees
|
|
1206
|
+
// a message once it is buffered): it closes without a grant instead of holding the message.
|
|
1207
|
+
// The loop ends in the same synchronous step as the push below, so nothing can slip in after it.
|
|
1208
|
+
for (let i = 0; i < 3 && holdingCards().length; i += 1) {
|
|
1209
|
+
await closeWithoutGrant(holdingCards());
|
|
1210
|
+
if (!state.currentKey) showNextCard();
|
|
1211
|
+
}
|
|
1212
|
+
// A newer prompt from the phone: the computer approval the running turn holds ends now, before
|
|
1213
|
+
// this message is buffered or queued (it would otherwise keep the Mac until the message starts).
|
|
1214
|
+
if (state.sessionId) { try { runtime()?.supersedeComputerApproval?.(state.sessionId); } catch (error) { log('whatsapp:bridge', `computer approval: ${error?.message || error}`); } }
|
|
1215
|
+
await pushPrompt(promptMessage);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* → true when the message was an answer (used up here); false when it goes on as a prompt.
|
|
1219
|
+
* A reply that quotes something is aimed at what it quotes: it can answer that request, while
|
|
1220
|
+
* it is open, and no other. Quoting a closed request, an ordinary message or one this link
|
|
1221
|
+
* does not know answers nothing: what is open closes without a grant and the message goes on.
|
|
1222
|
+
*/
|
|
1223
|
+
async function replyToPending(message, { answerable }) {
|
|
1224
|
+
const quotes = !!message.quoted;
|
|
1225
|
+
const quotedKey = message.quoted?.id ? state.cardByMessage.get(message.quoted.id) : null;
|
|
1226
|
+
const quotedCard = quotedKey ? state.cards.get(quotedKey) || null : null;
|
|
1227
|
+
// Nothing but an answer ("yes", "no", a bare option number): only that is a late answer to a
|
|
1228
|
+
// request that is gone. Anything that says more ("1. Explain the command first") is a message.
|
|
1229
|
+
const bare = answerable && isBareAnswer(message.text);
|
|
1230
|
+
// A bare yes / no to a request that is gone (answered on the desktop, timed out): say so; any other text is just the next message.
|
|
1231
|
+
if (quotedKey && !quotedCard && bare) { await say('Already answered in SynaBun.', { maxChunks: 1 }); return true; }
|
|
1232
|
+
let card = quotes ? quotedCard : currentCard();
|
|
1233
|
+
// A request whose message has not reached the phone yet (still being sent, throttled, waiting
|
|
1234
|
+
// in the outbox) cannot be what this message answers: the owner has not seen it. It closes
|
|
1235
|
+
// without a grant with everything else that is open, and the message goes on as a prompt.
|
|
1236
|
+
if (card && !card.messageIds.length) card = null;
|
|
1237
|
+
if (!card) {
|
|
1238
|
+
// Aimed at something that is not an open request: never an answer to one that is.
|
|
1239
|
+
if (quotes || holdingCards().length || currentCard()) { await moveOn(); return false; }
|
|
1240
|
+
const hidden = untrackedPending(state.sessionId);
|
|
1241
|
+
if (!hidden.length && bare && recentlyAnsweredElsewhere()) { await say('Already answered in SynaBun.', { maxChunks: 1 }); return true; }
|
|
1242
|
+
// Requests the phone never saw (a desktop turn's, or from before a restart of the link) must not hold the message.
|
|
1243
|
+
if (hidden.length) await moveOn();
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
if (answerable) {
|
|
1247
|
+
const result = answerCard(card, String(message.text ?? ''), { quoted: !!quotedCard });
|
|
1248
|
+
if (result.action === 'respond') return answer(card, result, message);
|
|
1249
|
+
if (result.action === 'invalid' || result.action === 'desktop') { await say(result.message, { maxChunks: 1 }); return true; }
|
|
1250
|
+
}
|
|
1251
|
+
await moveOn();
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
/** Into the coalescer (several quick messages are one prompt). → the prompt's handling when it flushed at once. */
|
|
1255
|
+
function pushPrompt(message) {
|
|
1256
|
+
const handle = (parts) => onCoalesced(parts).catch((error) => log('whatsapp:bridge', `prompt: ${error?.message || error}`));
|
|
1257
|
+
if (!coalescer && typeof inbound?.createCoalescer === 'function') {
|
|
1258
|
+
try { coalescer = inbound.createCoalescer({ windowMs: 1500, maxImages: 4, onFlush: (parts) => { state.buffered = 0; state.flushed = handle(parts); }, timers: T }); } catch { coalescer = null; }
|
|
1259
|
+
}
|
|
1260
|
+
const push = coalescer && (coalescer.push || coalescer.add || coalescer.feed);
|
|
1261
|
+
if (!push) return handle([message]);
|
|
1262
|
+
state.flushed = null;
|
|
1263
|
+
state.buffered += 1;
|
|
1264
|
+
push.call(coalescer, message);
|
|
1265
|
+
const flushed = state.flushed;
|
|
1266
|
+
state.flushed = null;
|
|
1267
|
+
return flushed || Promise.resolve();
|
|
1268
|
+
}
|
|
1269
|
+
async function onCoalesced(parts) {
|
|
1270
|
+
const list = (Array.isArray(parts) ? parts : Array.isArray(parts?.parts) ? parts.parts : Array.isArray(parts?.messages) ? parts.messages : [parts]).filter(Boolean);
|
|
1271
|
+
if (!list.length) return;
|
|
1272
|
+
let composed = null;
|
|
1273
|
+
try { composed = inbound?.composePrompt ? inbound.composePrompt(list, { rng }) : null; } catch (error) { log('whatsapp:bridge', `compose: ${error?.message || error}`); }
|
|
1274
|
+
if (!composed) composed = { text: list.map((part) => String(part.text || '')).filter(Boolean).join('\n\n'), images: list.flatMap((part) => part.images || []), sourceIds: list.map((part) => part.id).filter(Boolean), untrusted: list.some((part) => part.forwarded || part.quoted) };
|
|
1275
|
+
const images = Array.isArray(composed.images) ? composed.images.filter(Boolean) : [];
|
|
1276
|
+
const job = {
|
|
1277
|
+
text: String(composed.text || ''), images,
|
|
1278
|
+
// A picture carries content the owner did not type: the turn is capped at Ask.
|
|
1279
|
+
sourceIds: Array.isArray(composed.sourceIds) ? composed.sourceIds.filter(Boolean) : [], untrusted: composed.untrusted === true || images.length > 0,
|
|
1280
|
+
receivedAt: now(),
|
|
1281
|
+
};
|
|
1282
|
+
if (!job.text.trim() && !job.images.length) return;
|
|
1283
|
+
await enqueue(job);
|
|
1284
|
+
}
|
|
1285
|
+
/** Messages that wait together are one prompt: the Assistant reads them at once, like a person catching up. */
|
|
1286
|
+
function mergeJob(into, job) {
|
|
1287
|
+
if (!into || into.images.length + job.images.length > L.mergeImages || into.text.length + job.text.length > L.mergeChars) return false;
|
|
1288
|
+
into.text = [into.text, job.text].filter((part) => part && part.trim()).join('\n\n');
|
|
1289
|
+
into.images.push(...job.images);
|
|
1290
|
+
into.sourceIds.push(...job.sourceIds);
|
|
1291
|
+
into.untrusted = into.untrusted || job.untrusted;
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
async function enqueue(job, { head = false } = {}) {
|
|
1295
|
+
const busy = currentBusy() || state.starting;
|
|
1296
|
+
if (busy || state.queue.length || state.paused) {
|
|
1297
|
+
if (head || !mergeJob(state.queue[state.queue.length - 1], job)) {
|
|
1298
|
+
if (!head && state.queue.length >= L.queueMax) {
|
|
1299
|
+
react(job.sourceIds, 'failed');
|
|
1300
|
+
await say(`${L.queueMax} messages are already waiting: send this again when I am done.`, { maxChunks: 1 });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (head) state.queue.unshift(job); else state.queue.push(job);
|
|
1304
|
+
}
|
|
1305
|
+
react(job.sourceIds, 'queued');
|
|
1306
|
+
persistBridgeState();
|
|
1307
|
+
if (!busy && !state.paused) scheduleDrain(L.drainMs);
|
|
1308
|
+
watchQueue();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
await startJob(job);
|
|
1312
|
+
}
|
|
1313
|
+
function currentBusy() {
|
|
1314
|
+
const id = state.sessionId;
|
|
1315
|
+
if (!id) return false;
|
|
1316
|
+
let busy = false;
|
|
1317
|
+
try { busy = !!runtime()?.isBusy?.(id); } catch { busy = false; }
|
|
1318
|
+
const turn = state.turns.get(id);
|
|
1319
|
+
// A turn whose end never reached us (the runtime no longer runs it and it has been silent) is let go.
|
|
1320
|
+
if (turn && turn.state === 'RUNNING' && !busy && now() - turn.lastPacketAt > L.letGoMs) { finalizeTurn(id, 'done'); return true; }
|
|
1321
|
+
return busy || !!turn;
|
|
1322
|
+
}
|
|
1323
|
+
function scheduleDrain(ms) {
|
|
1324
|
+
if (state.drainTimer) T.clearTimeout(state.drainTimer);
|
|
1325
|
+
state.drainTimer = T.setTimeout(() => { state.drainTimer = null; drain().catch((error) => log('whatsapp:bridge', `drain: ${error?.message || error}`)); }, ms);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* While messages wait, try the queue again on a timer: the end of a turn
|
|
1329
|
+
* drains it at once, but a turn whose end never arrives (letGoMs) must not
|
|
1330
|
+
* hold a message until the owner writes again.
|
|
1331
|
+
*/
|
|
1332
|
+
function watchQueue() {
|
|
1333
|
+
if (state.queueTimer || state.stopped) return;
|
|
1334
|
+
state.queueTimer = T.setInterval(() => {
|
|
1335
|
+
if (!state.queue.length || state.stopped) { T.clearInterval(state.queueTimer); state.queueTimer = null; return; }
|
|
1336
|
+
drain().catch((error) => log('whatsapp:bridge', `queue watch: ${error?.message || error}`));
|
|
1337
|
+
}, L.queueWatchMs);
|
|
1338
|
+
state.queueTimer?.unref?.();
|
|
1339
|
+
}
|
|
1340
|
+
async function drain() {
|
|
1341
|
+
if (state.stopped || state.paused || state.starting || !state.queue.length || currentBusy()) return;
|
|
1342
|
+
const job = state.queue.shift();
|
|
1343
|
+
persistBridgeState();
|
|
1344
|
+
await startJob(job);
|
|
1345
|
+
}
|
|
1346
|
+
async function startJob(job, { retried = false } = {}) {
|
|
1347
|
+
const rt = runtime();
|
|
1348
|
+
if (!rt?.submit) { react(job.sourceIds, 'failed'); await say('The Assistant is not available right now.', { maxChunks: 1 }); return; }
|
|
1349
|
+
// One job at a time until its turn exists (creating the session awaits).
|
|
1350
|
+
state.starting = true;
|
|
1351
|
+
let sessionId;
|
|
1352
|
+
try { sessionId = await ensureSession(); } catch (error) {
|
|
1353
|
+
state.starting = false;
|
|
1354
|
+
react(job.sourceIds, 'failed');
|
|
1355
|
+
await say(`Could not start the Assistant: ${error?.message || error}`, { maxChunks: 1 });
|
|
1356
|
+
scheduleDrain(L.drainMs);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
// The brain Settings names, if it changed since this conversation was last put on it.
|
|
1360
|
+
await applyBrainChoice(sessionId).catch((error) => log('whatsapp:bridge', `brain choice: ${error?.message || error}`));
|
|
1361
|
+
await noticeBrainLimit(sessionId).catch(() => {});
|
|
1362
|
+
// The turn exists before submit: brain events can arrive before it resolves.
|
|
1363
|
+
const turn = makeTurn(sessionId, 'wa', { sourceIds: [...job.sourceIds] });
|
|
1364
|
+
state.starting = false;
|
|
1365
|
+
const latest = job.sourceIds[job.sourceIds.length - 1] || null;
|
|
1366
|
+
turn.replyTo = latest && latest !== state.lastInboundId ? latest : null;
|
|
1367
|
+
react(job.sourceIds, 'seen');
|
|
1368
|
+
state.inflight = { sourceIds: [...job.sourceIds], startedAt: now() };
|
|
1369
|
+
persistBridgeState();
|
|
1370
|
+
startProgress(turn);
|
|
1371
|
+
let result;
|
|
1372
|
+
try { result = await rt.submit(sessionId, { text: job.text, images: job.images, origin: 'whatsapp', authority: phoneAuthority, requireIdle: true, untrusted: job.untrusted }); }
|
|
1373
|
+
catch (error) { result = { ok: false, code: error?.code || 'SUBMIT_FAILED', message: error?.message || String(error) }; }
|
|
1374
|
+
if (state.turns.get(sessionId) !== turn) return; // finalized meanwhile (watchdog, /stop)
|
|
1375
|
+
if (result?.ok) {
|
|
1376
|
+
turn.state = 'RUNNING';
|
|
1377
|
+
if (turn.endedEarly) finalizeTurn(sessionId, turn.endedEarly.outcome, { message: turn.endedEarly.message });
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
// Not submitted: this turn never ran.
|
|
1381
|
+
state.turns.delete(sessionId);
|
|
1382
|
+
disarm(turn);
|
|
1383
|
+
presence('paused');
|
|
1384
|
+
state.inflight = null;
|
|
1385
|
+
await onRefused(job, result || {}, { retried, sessionId });
|
|
1386
|
+
}
|
|
1387
|
+
async function onRefused(job, result, { retried, sessionId }) {
|
|
1388
|
+
const code = result.code || 'UNKNOWN';
|
|
1389
|
+
if (code === 'ASSISTANT_BUSY') {
|
|
1390
|
+
// Something else is running (a desktop turn, the mailbox): first in line again.
|
|
1391
|
+
state.queue.unshift(job);
|
|
1392
|
+
react(job.sourceIds, 'queued');
|
|
1393
|
+
persistBridgeState();
|
|
1394
|
+
if (state.busyRetryTimer) T.clearTimeout(state.busyRetryTimer);
|
|
1395
|
+
state.busyRetryTimer = T.setTimeout(() => { state.busyRetryTimer = null; drain().catch(() => {}); }, L.busyRetryMs);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if ((code === 'SESSION_NOT_FOUND' || code === 'SESSION_ENDED') && !retried) {
|
|
1399
|
+
if (state.sessionId === sessionId) state.sessionId = null;
|
|
1400
|
+
await startJob(job, { retried: true });
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
persistBridgeState();
|
|
1404
|
+
react(job.sourceIds, 'failed');
|
|
1405
|
+
if (code === 'SESSION_BUDGET_EXCEEDED' || code === 'BRAIN_BUDGET_EXCEEDED') { await say(budgetText(code, sessionId, result.message), { maxChunks: 1 }); return; }
|
|
1406
|
+
if (code === 'EMPTY_PROMPT') return;
|
|
1407
|
+
await say(`Could not start that: ${String(result.message || code).slice(0, 400)}`, { maxChunks: 1 });
|
|
1408
|
+
}
|
|
1409
|
+
/** Spend against the cap, and the two ways on: /new or a higher cap. Never a silent new session. */
|
|
1410
|
+
function budgetText(code, sessionId, fallback) {
|
|
1411
|
+
const view = sessionView(sessionId);
|
|
1412
|
+
let line = '';
|
|
1413
|
+
if (code === 'SESSION_BUDGET_EXCEEDED' && view?.budget && Number.isFinite(Number(view.budget.hardUsd))) {
|
|
1414
|
+
line = `This conversation spent ${usd(view.budget.totalUsd)} of its ${usd(view.budget.hardUsd)} cap.`;
|
|
1415
|
+
} else if (code === 'BRAIN_BUDGET_EXCEEDED') {
|
|
1416
|
+
let cap = null;
|
|
1417
|
+
try { cap = runtime()?.brainCapUsd?.() ?? null; } catch { cap = null; }
|
|
1418
|
+
line = cap !== null ? `The Assistant spent ${usd(view?.costUsd)} of its ${usd(cap)} cap in this conversation.` : '';
|
|
1419
|
+
}
|
|
1420
|
+
return `${line || String(fallback || 'This conversation reached its budget cap.')} Send /new to start a fresh conversation, or raise the cap in SynaBun.`;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
1424
|
+
/** The transport's connection changed ({ connected } or a state name). */
|
|
1425
|
+
async function onConnection(info) {
|
|
1426
|
+
const connected = typeof info === 'boolean' ? info : info?.connected === true || info?.state === 'open';
|
|
1427
|
+
if (!connected) { presence('paused'); return; }
|
|
1428
|
+
runtime();
|
|
1429
|
+
syncPolicies();
|
|
1430
|
+
if (state.restartNotice) {
|
|
1431
|
+
const notice = state.restartNotice;
|
|
1432
|
+
state.restartNotice = null;
|
|
1433
|
+
const from = notice.inflight?.startedAt ? hhmm(Number(notice.inflight.startedAt)) : null;
|
|
1434
|
+
const parts = [];
|
|
1435
|
+
if (from) parts.push(`SynaBun restarted while working on your message from ${from}.`);
|
|
1436
|
+
if (notice.queued) parts.push(`${notice.queued} queued message${notice.queued === 1 ? ' was' : 's were'} not sent to the Assistant.`);
|
|
1437
|
+
parts.push('Send again what you still need.');
|
|
1438
|
+
state.inflight = null;
|
|
1439
|
+
persistBridgeState();
|
|
1440
|
+
await say(parts.join(' '), { maxChunks: 1 });
|
|
1441
|
+
}
|
|
1442
|
+
await flushOutbox();
|
|
1443
|
+
}
|
|
1444
|
+
function status() {
|
|
1445
|
+
const id = state.sessionId;
|
|
1446
|
+
let running = !!(id && state.turns.has(id));
|
|
1447
|
+
if (!running && id) { try { running = !!runtimeRef?.isBusy?.(id); } catch {} }
|
|
1448
|
+
return { sessionId: id || null, running, queued: state.queue.length, paused: state.paused, pausedBy: state.pausedBy, pendingCards: state.cards.size, lastReplyAt: state.lastReplyAt };
|
|
1449
|
+
}
|
|
1450
|
+
async function stopAll() {
|
|
1451
|
+
const result = await stopEverything({ reason: 'service' });
|
|
1452
|
+
return { ok: true, ...result };
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Live computer control ends with the bridge. Every WhatsApp session of ours, current and retired,
|
|
1456
|
+
* that is registered with computer use is registered again without it; the runtime hears that at
|
|
1457
|
+
* once: an open "control your Mac?" request closes without a grant, the desktop grant is held again,
|
|
1458
|
+
* the action in flight is aborted, held input and the lease are released, and the running turn is
|
|
1459
|
+
* done with the Mac for good. Without this an unlink left an approved turn with the Mac and no
|
|
1460
|
+
* phone to stop it. It sticks: nothing this bridge registers afterwards carries computer use.
|
|
1461
|
+
*/
|
|
1462
|
+
function endComputerUse(reason = 'bridge dropped') {
|
|
1463
|
+
state.computerEnded = true;
|
|
1464
|
+
if (!policy?.registerSessionPolicy) return;
|
|
1465
|
+
const known = typeof policy.getSessionPolicy === 'function';
|
|
1466
|
+
const off = { ...remoteFromConfig(), computerUse: false };
|
|
1467
|
+
for (const id of [state.sessionId, ...state.previous.map((row) => row.id)].filter(Boolean)) {
|
|
1468
|
+
if (known ? policy.getSessionPolicy(id)?.computerUse !== true : id !== state.sessionId) continue;
|
|
1469
|
+
try { policy.registerSessionPolicy(id, off); state.policySeen.delete(id); log('whatsapp:bridge', `computer use of ${id} ended (${reason})`); }
|
|
1470
|
+
catch (error) { log('whatsapp:bridge', `ending computer use of ${id}: ${error?.message || error}`); }
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
async function shutdown() {
|
|
1474
|
+
state.stopped = true;
|
|
1475
|
+
endComputerUse('bridge shut down'); // before anything else, and before any await
|
|
1476
|
+
for (const turn of state.turns.values()) disarm(turn);
|
|
1477
|
+
for (const timer of [state.drainTimer, state.busyRetryTimer, state.staleTimer, state.dispatchTimer, state.forwardSummaryTimer, state.outboxTimer]) if (timer) T.clearTimeout(timer);
|
|
1478
|
+
if (state.policyTimer) T.clearInterval(state.policyTimer);
|
|
1479
|
+
if (state.queueTimer) { T.clearInterval(state.queueTimer); state.queueTimer = null; }
|
|
1480
|
+
try { unobserve?.(); } catch {}
|
|
1481
|
+
try { unsubscribeRuntime?.(); } catch {}
|
|
1482
|
+
try { coalescer?.flush?.(); } catch {}
|
|
1483
|
+
presence('paused');
|
|
1484
|
+
persistBridgeState();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Keep the level in line with the config (Settings edits, the autonomous window running out).
|
|
1488
|
+
state.policyTimer = T.setInterval(() => { try { runtime(); syncPolicies(); } catch {} }, L.policyTickMs);
|
|
1489
|
+
state.policyTimer?.unref?.();
|
|
1490
|
+
runtime();
|
|
1491
|
+
|
|
1492
|
+
return {
|
|
1493
|
+
onInbound, onConnection, status, newSession, pause, resume, stopAll, shutdown,
|
|
1494
|
+
// Settings changed (level, strict approvals, autonomous window): re-apply now instead of at the next tick.
|
|
1495
|
+
refresh: () => { state.policySeen.clear(); runtime(); syncPolicies(); return status(); },
|
|
1496
|
+
endComputerUse,
|
|
1497
|
+
_internals: { state, finalText, syncPolicies, drain, onPacket, brainTarget, moveOn },
|
|
1498
|
+
};
|
|
1499
|
+
}
|