cade-cli 0.15.8__tar.gz → 0.15.9__tar.gz
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.
- {cade_cli-0.15.8 → cade_cli-0.15.9}/PKG-INFO +1 -1
- {cade_cli-0.15.8 → cade_cli-0.15.9}/pyproject.toml +1 -1
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/serve.py +3 -2
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/imessage.py +120 -11
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/config.py +9 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/serve/serve.toml.example +3 -1
- {cade_cli-0.15.8 → cade_cli-0.15.9}/.gitignore +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/LICENSE +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/README.md +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/__main__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/_metadata.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/config.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/errors.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/resources.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/server.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/_read_cache.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/agents.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/context.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/filesystem.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/git.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/notifications.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/questions.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/results.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/schemas.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/search.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/shell.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/snippets.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/tasks.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/web_fetch.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/web_search.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/utils.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/arcade_client.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/cache.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/extract.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/markdown.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/preapproved.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/prompts.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/scrape.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/arcade.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/base.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/http.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ai/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ai/prompts.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/app.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/auth.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/account.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/agents.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/auth.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/chat.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/cron.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/hooks.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/mcp.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/mem.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/tasks.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/thread.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/tools.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/onboarding.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/agent.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/comms.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/config.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/constants.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/errors.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/frozen.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/git.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/inbox.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/logging.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/memory.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/model_specs.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/names.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/paths.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/runtime.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/secrets.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/spawn.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/types.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/context_window.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/mcp_schema_index.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/orchestrator.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/parallel.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/tool_result_store.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/tool_schema_cache.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/config.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/engine.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/callback.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/command.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/http.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/prompt.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/ssrf_guard.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/matchers.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/registry.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/types.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/anthropic.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/base.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/ollama.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/openai.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/base.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/desktop.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/registry.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/stub.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/telegram.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/allowlist.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/chunker.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/commands.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/cron.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/daemon.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/elicitation.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/format.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/install.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/lock.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/observability.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/owner.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/progress_sink.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/router.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/secrets.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/services.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/sinks.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/worker.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/storage/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/storage/threads.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/lock.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/scheduler.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/store.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/types.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/login_failed.html +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/login_success.html +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/serve/launchd.plist.tmpl +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/serve/systemd.service.tmpl +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/styles.css +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/_request_ctx.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/base.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/composite.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/config.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/mcp.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/discovered.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/scoring.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/service.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/completion.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/display.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/elicitation.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/input.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/session.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/slash.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/__init__.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/audio.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/cleanup.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/session.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/stt.py +0 -0
- {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/tts.py +0 -0
|
@@ -204,7 +204,7 @@ def _init_imessage(cfg) -> None: # noqa: ANN001 — ServeConfig, kept lazy for
|
|
|
204
204
|
binary = shutil.which(imessage.cli_path or "imsg")
|
|
205
205
|
if binary is None:
|
|
206
206
|
console.print("[yellow]imsg is not installed.[/yellow]")
|
|
207
|
-
console.print("Install it first: [bold]brew install imsg[/bold]")
|
|
207
|
+
console.print("Install it first: [bold]brew install steipete/tap/imsg[/bold]")
|
|
208
208
|
console.print("[dim]Then re-run `cade serve init`. Skipping iMessage setup.[/dim]")
|
|
209
209
|
return
|
|
210
210
|
|
|
@@ -236,7 +236,8 @@ def _init_imessage(cfg) -> None: # noqa: ANN001 — ServeConfig, kept lazy for
|
|
|
236
236
|
# mirroring the Telegram wizard — an empty list means nobody is answered.
|
|
237
237
|
console.print(
|
|
238
238
|
"[dim]Allowed senders: phone numbers (+15551234567) or iMessage emails. "
|
|
239
|
-
"Only these people can talk to the agent.
|
|
239
|
+
"Only these people can talk to the agent. Enter your own number/email "
|
|
240
|
+
"to chat with the agent in your Note-to-Self thread.[/dim]"
|
|
240
241
|
)
|
|
241
242
|
while True:
|
|
242
243
|
raw = typer.prompt("Allowed sender (phone/email)").strip()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Apple iMessage adapter — bridges `cade serve` to Messages.app via `imsg`.
|
|
2
2
|
|
|
3
3
|
macOS-only. All platform work is delegated to the `imsg` CLI
|
|
4
|
-
(https://imsg.sh, `brew install imsg`), which reads inbound
|
|
4
|
+
(https://imsg.sh, `brew install steipete/tap/imsg`), which reads inbound
|
|
5
|
+
messages from
|
|
5
6
|
`~/Library/Messages/chat.db` and sends through Messages.app's AppleScript
|
|
6
7
|
surface. The adapter runs one long-lived `imsg rpc` subprocess speaking
|
|
7
8
|
JSON-RPC 2.0 over stdio (one JSON object per line) and consumes its
|
|
@@ -27,6 +28,17 @@ Permissions (see docs/adapters/imessage.md):
|
|
|
27
28
|
Typing indicators are best-effort: they need the imsg IMCore bridge (SIP
|
|
28
29
|
disabled + `imsg launch`), which most machines won't have. The adapter
|
|
29
30
|
probes `imsg status --json` once at start and no-ops when unavailable.
|
|
31
|
+
|
|
32
|
+
Self-chat (texting your own Apple ID) is supported and on by default: when
|
|
33
|
+
the Mac shares the user's personal account, messages they send to their own
|
|
34
|
+
handle (the "Note to Self" thread) count as inbound turns. Loop safety comes
|
|
35
|
+
from tracking what the adapter itself sent — echo rows are dropped by sent
|
|
36
|
+
guid, with a recently-sent-text fallback for the rare send whose guid the
|
|
37
|
+
bridge couldn't report. Outgoing rows in *other* chats (the user texting
|
|
38
|
+
friends, synced from any device) are never treated as inbound: a self-chat
|
|
39
|
+
turn additionally requires the chat's handle to be one of the account's own
|
|
40
|
+
handles, learned from ``destination_caller_id`` on outgoing traffic (or set
|
|
41
|
+
explicitly via ``self_handles``).
|
|
30
42
|
"""
|
|
31
43
|
|
|
32
44
|
from __future__ import annotations
|
|
@@ -38,6 +50,7 @@ import json
|
|
|
38
50
|
import re
|
|
39
51
|
import shutil
|
|
40
52
|
import time
|
|
53
|
+
from collections import deque
|
|
41
54
|
from collections.abc import AsyncIterator
|
|
42
55
|
from pathlib import Path
|
|
43
56
|
from typing import Any
|
|
@@ -62,6 +75,12 @@ _TYPING_DURATION = "5s"
|
|
|
62
75
|
_TYPING_MAX_FAILURES = 3
|
|
63
76
|
_RECONNECT_MAX_ATTEMPTS = 5
|
|
64
77
|
_VERIFY_DELAY_SECONDS = 5.0
|
|
78
|
+
# Echo-suppression state for self-chat mode: how many of our own sent
|
|
79
|
+
# message guids to remember, and how long a sent text stays matchable for
|
|
80
|
+
# the fallback path (sends whose guid the bridge couldn't report).
|
|
81
|
+
_SENT_GUID_LIMIT = 500
|
|
82
|
+
_SENT_TEXT_LIMIT = 64
|
|
83
|
+
_SENT_TEXT_TTL_SECONDS = 180.0
|
|
65
84
|
|
|
66
85
|
_PERMISSION_HINT = (
|
|
67
86
|
"imsg could not read the Messages database. Grant Full Disk Access to the "
|
|
@@ -87,6 +106,10 @@ def canonical_handle(handle: str, region: str = "US") -> str:
|
|
|
87
106
|
from inbound traffic, not perfect international parsing.
|
|
88
107
|
"""
|
|
89
108
|
value = (handle or "").strip()
|
|
109
|
+
# Messages sometimes prefixes routing handles with the alias type
|
|
110
|
+
# ("P:+1555…", "e:alice@…"); strip it so they compare like bare handles.
|
|
111
|
+
if len(value) > 2 and value[1] == ":" and value[0].lower() in ("p", "e"):
|
|
112
|
+
value = value[2:].strip()
|
|
90
113
|
if not value:
|
|
91
114
|
return ""
|
|
92
115
|
if "@" in value:
|
|
@@ -180,6 +203,15 @@ class IMessageAdapter:
|
|
|
180
203
|
}
|
|
181
204
|
self._allowed_chats = set(settings.allowed_chats or [])
|
|
182
205
|
self._allowed_services = {s.strip().lower() for s in (settings.allowed_services or [])}
|
|
206
|
+
# Self-chat support: this account's own handles (seeded from config,
|
|
207
|
+
# auto-learned from destination_caller_id on outgoing rows) plus the
|
|
208
|
+
# echo-suppression records of what this adapter itself sent.
|
|
209
|
+
self._own_handles = {
|
|
210
|
+
canonical_handle(h, region) for h in (settings.self_handles or []) if h.strip()
|
|
211
|
+
}
|
|
212
|
+
self._sent_guids: set[str] = set()
|
|
213
|
+
self._sent_guid_order: deque[str] = deque()
|
|
214
|
+
self._sent_texts: deque[tuple[str, float]] = deque(maxlen=_SENT_TEXT_LIMIT)
|
|
183
215
|
|
|
184
216
|
# ------------------------------------------------------------- lifecycle
|
|
185
217
|
|
|
@@ -250,12 +282,42 @@ class IMessageAdapter:
|
|
|
250
282
|
params["region"] = self.settings.region
|
|
251
283
|
|
|
252
284
|
await self._stop_typing(target)
|
|
285
|
+
# Record the text BEFORE the rpc call: the watch stream can echo our
|
|
286
|
+
# own send back faster than the send response arrives, and self-chat
|
|
287
|
+
# mode must never mistake that echo for a new user turn.
|
|
288
|
+
self._note_sent_text(msg.text)
|
|
253
289
|
result = await self._rpc("send", params)
|
|
254
290
|
guid = str(result.get("guid") or "")
|
|
255
|
-
if guid
|
|
256
|
-
self.
|
|
291
|
+
if guid:
|
|
292
|
+
self._remember_sent_guid(guid)
|
|
293
|
+
if self.settings.verify_delivery:
|
|
294
|
+
self._spawn_background(self._verify_delivery(guid), name="imessage-verify")
|
|
257
295
|
return guid or str(result.get("id") or "")
|
|
258
296
|
|
|
297
|
+
# ------------------------------------------------------ echo suppression
|
|
298
|
+
|
|
299
|
+
def _remember_sent_guid(self, guid: str) -> None:
|
|
300
|
+
"""Track a message guid this adapter sent (bounded)."""
|
|
301
|
+
if guid in self._sent_guids:
|
|
302
|
+
return
|
|
303
|
+
self._sent_guids.add(guid)
|
|
304
|
+
self._sent_guid_order.append(guid)
|
|
305
|
+
while len(self._sent_guid_order) > _SENT_GUID_LIMIT:
|
|
306
|
+
self._sent_guids.discard(self._sent_guid_order.popleft())
|
|
307
|
+
|
|
308
|
+
def _note_sent_text(self, text: str) -> None:
|
|
309
|
+
"""Track sent text for the guid-less echo fallback (bounded + TTL)."""
|
|
310
|
+
self._sent_texts.append((text.strip(), time.monotonic()))
|
|
311
|
+
|
|
312
|
+
def _matches_recent_send(self, text: str) -> bool:
|
|
313
|
+
"""True (consuming the record) when ``text`` matches a recent send."""
|
|
314
|
+
now = time.monotonic()
|
|
315
|
+
for index, (sent_text, sent_at) in enumerate(self._sent_texts):
|
|
316
|
+
if now - sent_at <= _SENT_TEXT_TTL_SECONDS and sent_text == text:
|
|
317
|
+
del self._sent_texts[index]
|
|
318
|
+
return True
|
|
319
|
+
return False
|
|
320
|
+
|
|
259
321
|
async def _verify_delivery(self, guid: str) -> None:
|
|
260
322
|
"""Silent-failure guard: surface sends Messages accepted but dropped."""
|
|
261
323
|
await asyncio.sleep(_VERIFY_DELAY_SECONDS)
|
|
@@ -395,7 +457,7 @@ class IMessageAdapter:
|
|
|
395
457
|
return found
|
|
396
458
|
raise IMessageBridgeError(
|
|
397
459
|
f"imsg binary not found (cli_path={configured!r}). "
|
|
398
|
-
"Install it with `brew install imsg` or set cli_path in "
|
|
460
|
+
"Install it with `brew install steipete/tap/imsg` or set cli_path in "
|
|
399
461
|
"[serve.adapters.imessage]."
|
|
400
462
|
)
|
|
401
463
|
|
|
@@ -503,15 +565,23 @@ class IMessageAdapter:
|
|
|
503
565
|
if isinstance(chat_id, int) and chat_id > 0:
|
|
504
566
|
self._chat_rowids[chat_guid] = chat_id
|
|
505
567
|
|
|
506
|
-
# The watch stream echoes our own sends; without this the agent
|
|
507
|
-
# would answer itself in an infinite loop.
|
|
508
|
-
if message.get("is_from_me"):
|
|
509
|
-
return
|
|
510
568
|
if message.get("is_reaction"):
|
|
511
569
|
return
|
|
512
570
|
text = str(message.get("text") or "").strip()
|
|
513
571
|
if not text:
|
|
514
572
|
return
|
|
573
|
+
region = self.settings.region or "US"
|
|
574
|
+
|
|
575
|
+
is_from_me = bool(message.get("is_from_me"))
|
|
576
|
+
if is_from_me:
|
|
577
|
+
# Learn this account's own handles from outgoing routing metadata
|
|
578
|
+
# regardless of whether self-chat is enabled — it costs nothing
|
|
579
|
+
# and makes the self-chat check reliable from the first message.
|
|
580
|
+
caller_id = canonical_handle(str(message.get("destination_caller_id") or ""), region)
|
|
581
|
+
if caller_id:
|
|
582
|
+
self._own_handles.add(caller_id)
|
|
583
|
+
if not self._accept_self_chat_message(message, chat_guid, text):
|
|
584
|
+
return
|
|
515
585
|
|
|
516
586
|
service = chat_guid.split(";", 1)[0].strip().lower()
|
|
517
587
|
if self._allowed_services and service not in self._allowed_services:
|
|
@@ -524,14 +594,24 @@ class IMessageAdapter:
|
|
|
524
594
|
log.debug(f"iMessage dropped group message in {chat_guid} (allow_groups=false)")
|
|
525
595
|
return
|
|
526
596
|
|
|
527
|
-
|
|
597
|
+
if is_from_me:
|
|
598
|
+
# Self-chat turn: the "sender" is the account owner, identified by
|
|
599
|
+
# the thread's own handle (the raw sender column is often empty on
|
|
600
|
+
# self-sent rows).
|
|
601
|
+
sender_id = canonical_handle(handle_for_conversation(chat_guid) or "", region)
|
|
602
|
+
sender_name = str(message.get("sender_name") or "Me")
|
|
603
|
+
else:
|
|
604
|
+
sender_raw = str(message.get("sender") or "")
|
|
605
|
+
sender_id = canonical_handle(sender_raw, region)
|
|
606
|
+
sender_name = str(message.get("sender_name") or sender_raw or "unknown")
|
|
607
|
+
|
|
528
608
|
guid = str(message.get("guid") or rowid)
|
|
529
609
|
self._inbound.put_nowait(
|
|
530
610
|
InboundMessage(
|
|
531
611
|
adapter=self.name,
|
|
532
612
|
conversation_id=chat_guid,
|
|
533
|
-
sender_id=
|
|
534
|
-
sender_name=
|
|
613
|
+
sender_id=sender_id,
|
|
614
|
+
sender_name=sender_name,
|
|
535
615
|
text=text,
|
|
536
616
|
message_id=guid,
|
|
537
617
|
reply_to_id=(
|
|
@@ -540,6 +620,35 @@ class IMessageAdapter:
|
|
|
540
620
|
)
|
|
541
621
|
)
|
|
542
622
|
|
|
623
|
+
def _accept_self_chat_message(self, message: dict[str, Any], chat_guid: str, text: str) -> bool:
|
|
624
|
+
"""Decide whether an outgoing (``is_from_me``) row is a self-chat turn.
|
|
625
|
+
|
|
626
|
+
Loop safety first: anything this adapter itself sent is dropped, by
|
|
627
|
+
guid when the bridge reported one, else by matching recently-sent
|
|
628
|
+
text. What remains is only accepted as inbound when self-chat is
|
|
629
|
+
enabled, the thread is a DM, and the thread's handle is one of the
|
|
630
|
+
account's own handles — i.e. the user texting their own Apple ID.
|
|
631
|
+
Outgoing messages to anyone else are never turns.
|
|
632
|
+
"""
|
|
633
|
+
guid = str(message.get("guid") or "")
|
|
634
|
+
if guid and guid in self._sent_guids:
|
|
635
|
+
# Consume the matching text record too, so a user later typing
|
|
636
|
+
# the same text as our reply isn't swallowed by the fallback.
|
|
637
|
+
self._matches_recent_send(text)
|
|
638
|
+
return False
|
|
639
|
+
if not self.settings.self_chat:
|
|
640
|
+
return False
|
|
641
|
+
if is_group_conversation(chat_guid):
|
|
642
|
+
return False
|
|
643
|
+
own_handle = canonical_handle(
|
|
644
|
+
handle_for_conversation(chat_guid) or "", self.settings.region or "US"
|
|
645
|
+
)
|
|
646
|
+
if not own_handle or own_handle not in self._own_handles:
|
|
647
|
+
return False
|
|
648
|
+
if self._matches_recent_send(text):
|
|
649
|
+
return False
|
|
650
|
+
return True
|
|
651
|
+
|
|
543
652
|
async def _rpc(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
544
653
|
"""Send one JSON-RPC request and await its response.
|
|
545
654
|
|
|
@@ -94,6 +94,15 @@ class IMessageAdapterSettings(BaseAdapterSettings):
|
|
|
94
94
|
# Check message.send_status a few seconds after each send and log an
|
|
95
95
|
# error when Messages reports the send failed (silent-failure guard).
|
|
96
96
|
verify_delivery: bool = True
|
|
97
|
+
# Answer in the user's own "Note to Self" thread when the Mac shares
|
|
98
|
+
# their personal Apple ID (no dedicated agent account). Loop-safe: the
|
|
99
|
+
# adapter's own replies are filtered by sent-message guid/text tracking.
|
|
100
|
+
# The user's own handle must still be in allowed_senders. DM-only.
|
|
101
|
+
self_chat: bool = True
|
|
102
|
+
# This account's own handles (phone/email) for self-chat detection.
|
|
103
|
+
# Usually left empty — they are learned automatically from outgoing
|
|
104
|
+
# message routing metadata. Set explicitly if detection misses.
|
|
105
|
+
self_handles: list[str] = Field(default_factory=list)
|
|
97
106
|
|
|
98
107
|
|
|
99
108
|
def _discover_adapters() -> dict[str, type[BaseAdapterSettings]]:
|
|
@@ -40,7 +40,7 @@ allowed_conversations = [] # optional: restrict desktop chat ids
|
|
|
40
40
|
speak_replies = false # opt in to local Kokoro TTS replies
|
|
41
41
|
|
|
42
42
|
[serve.adapters.imessage]
|
|
43
|
-
# macOS only. Requires `brew install imsg` plus Full Disk Access for the
|
|
43
|
+
# macOS only. Requires `brew install steipete/tap/imsg` plus Full Disk Access for the
|
|
44
44
|
# process running `cade serve` (see docs/adapters/imessage.md).
|
|
45
45
|
enabled = false
|
|
46
46
|
cli_path = "imsg"
|
|
@@ -54,6 +54,8 @@ service = "imessage" # outbound service for direct sends
|
|
|
54
54
|
region = "US" # country code for phone normalization
|
|
55
55
|
typing_indicators = true # best-effort; needs the optional imsg bridge
|
|
56
56
|
verify_delivery = true # log an error when Messages drops a send
|
|
57
|
+
self_chat = true # answer in your own Note-to-Self thread
|
|
58
|
+
self_handles = [] # your own handles; auto-learned when empty
|
|
57
59
|
|
|
58
60
|
# Additional adapters appear under [serve.adapters.<name>]. The full set of
|
|
59
61
|
# supported adapters is whatever is registered in
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|