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.
Files changed (166) hide show
  1. {cade_cli-0.15.8 → cade_cli-0.15.9}/PKG-INFO +1 -1
  2. {cade_cli-0.15.8 → cade_cli-0.15.9}/pyproject.toml +1 -1
  3. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/serve.py +3 -2
  4. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/imessage.py +120 -11
  5. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/config.py +9 -0
  6. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/serve/serve.toml.example +3 -1
  7. {cade_cli-0.15.8 → cade_cli-0.15.9}/.gitignore +0 -0
  8. {cade_cli-0.15.8 → cade_cli-0.15.9}/LICENSE +0 -0
  9. {cade_cli-0.15.8 → cade_cli-0.15.9}/README.md +0 -0
  10. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/__init__.py +0 -0
  11. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/__main__.py +0 -0
  12. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/_metadata.py +0 -0
  13. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/config.py +0 -0
  14. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/errors.py +0 -0
  15. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/resources.py +0 -0
  16. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/server.py +0 -0
  17. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/__init__.py +0 -0
  18. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/_read_cache.py +0 -0
  19. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/agents.py +0 -0
  20. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/context.py +0 -0
  21. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/filesystem.py +0 -0
  22. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/git.py +0 -0
  23. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/notifications.py +0 -0
  24. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/questions.py +0 -0
  25. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/results.py +0 -0
  26. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/schemas.py +0 -0
  27. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/search.py +0 -0
  28. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/shell.py +0 -0
  29. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/snippets.py +0 -0
  30. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/tasks.py +0 -0
  31. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/web_fetch.py +0 -0
  32. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/tools/web_search.py +0 -0
  33. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/utils.py +0 -0
  34. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/__init__.py +0 -0
  35. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/arcade_client.py +0 -0
  36. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/cache.py +0 -0
  37. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/extract.py +0 -0
  38. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/markdown.py +0 -0
  39. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/preapproved.py +0 -0
  40. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/prompts.py +0 -0
  41. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/scrape.py +0 -0
  42. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/__init__.py +0 -0
  43. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/arcade.py +0 -0
  44. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/base.py +0 -0
  45. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cade_mcp_local/web/search/http.py +0 -0
  46. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/__init__.py +0 -0
  47. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ai/__init__.py +0 -0
  48. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ai/prompts.py +0 -0
  49. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/__init__.py +0 -0
  50. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/app.py +0 -0
  51. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/auth.py +0 -0
  52. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/__init__.py +0 -0
  53. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/account.py +0 -0
  54. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/agents.py +0 -0
  55. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/auth.py +0 -0
  56. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/chat.py +0 -0
  57. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/cron.py +0 -0
  58. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/hooks.py +0 -0
  59. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/mcp.py +0 -0
  60. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/mem.py +0 -0
  61. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/tasks.py +0 -0
  62. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/thread.py +0 -0
  63. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/commands/tools.py +0 -0
  64. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/cli/onboarding.py +0 -0
  65. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/__init__.py +0 -0
  66. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/agent.py +0 -0
  67. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/comms.py +0 -0
  68. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/config.py +0 -0
  69. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/constants.py +0 -0
  70. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/errors.py +0 -0
  71. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/frozen.py +0 -0
  72. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/git.py +0 -0
  73. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/inbox.py +0 -0
  74. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/logging.py +0 -0
  75. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/memory.py +0 -0
  76. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/model_specs.py +0 -0
  77. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/names.py +0 -0
  78. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/paths.py +0 -0
  79. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/runtime.py +0 -0
  80. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/secrets.py +0 -0
  81. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/spawn.py +0 -0
  82. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/core/types.py +0 -0
  83. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/__init__.py +0 -0
  84. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/context_window.py +0 -0
  85. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/mcp_schema_index.py +0 -0
  86. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/orchestrator.py +0 -0
  87. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/parallel.py +0 -0
  88. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/tool_result_store.py +0 -0
  89. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/execution/tool_schema_cache.py +0 -0
  90. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/__init__.py +0 -0
  91. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/config.py +0 -0
  92. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/engine.py +0 -0
  93. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/__init__.py +0 -0
  94. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/callback.py +0 -0
  95. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/command.py +0 -0
  96. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/http.py +0 -0
  97. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/prompt.py +0 -0
  98. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/executors/ssrf_guard.py +0 -0
  99. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/matchers.py +0 -0
  100. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/registry.py +0 -0
  101. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/hooks/types.py +0 -0
  102. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/__init__.py +0 -0
  103. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/anthropic.py +0 -0
  104. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/base.py +0 -0
  105. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/ollama.py +0 -0
  106. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/providers/openai.py +0 -0
  107. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/__init__.py +0 -0
  108. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/__init__.py +0 -0
  109. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/base.py +0 -0
  110. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/desktop.py +0 -0
  111. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/registry.py +0 -0
  112. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/stub.py +0 -0
  113. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/adapters/telegram.py +0 -0
  114. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/allowlist.py +0 -0
  115. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/chunker.py +0 -0
  116. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/commands.py +0 -0
  117. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/cron.py +0 -0
  118. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/daemon.py +0 -0
  119. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/elicitation.py +0 -0
  120. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/format.py +0 -0
  121. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/install.py +0 -0
  122. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/lock.py +0 -0
  123. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/observability.py +0 -0
  124. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/owner.py +0 -0
  125. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/progress_sink.py +0 -0
  126. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/router.py +0 -0
  127. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/secrets.py +0 -0
  128. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/services.py +0 -0
  129. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/sinks.py +0 -0
  130. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/serve/worker.py +0 -0
  131. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/storage/__init__.py +0 -0
  132. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/storage/threads.py +0 -0
  133. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/__init__.py +0 -0
  134. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/lock.py +0 -0
  135. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/scheduler.py +0 -0
  136. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/store.py +0 -0
  137. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tasks/types.py +0 -0
  138. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/login_failed.html +0 -0
  139. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/login_success.html +0 -0
  140. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/serve/launchd.plist.tmpl +0 -0
  141. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/serve/systemd.service.tmpl +0 -0
  142. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/templates/styles.css +0 -0
  143. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/__init__.py +0 -0
  144. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/__init__.py +0 -0
  145. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/_request_ctx.py +0 -0
  146. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/base.py +0 -0
  147. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/composite.py +0 -0
  148. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/config.py +0 -0
  149. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/manager/mcp.py +0 -0
  150. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/__init__.py +0 -0
  151. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/discovered.py +0 -0
  152. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/scoring.py +0 -0
  153. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/tools/search/service.py +0 -0
  154. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/__init__.py +0 -0
  155. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/completion.py +0 -0
  156. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/display.py +0 -0
  157. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/elicitation.py +0 -0
  158. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/input.py +0 -0
  159. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/session.py +0 -0
  160. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/ui/slash.py +0 -0
  161. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/__init__.py +0 -0
  162. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/audio.py +0 -0
  163. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/cleanup.py +0 -0
  164. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/session.py +0 -0
  165. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/stt.py +0 -0
  166. {cade_cli-0.15.8 → cade_cli-0.15.9}/src/cadecoder/voice/tts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cade-cli
3
- Version: 0.15.8
3
+ Version: 0.15.9
4
4
  Summary: Cade - The CLI Agent from Arcade.dev
5
5
  Project-URL: Homepage, https://arcade.dev
6
6
  Project-URL: Documentation, https://docs.arcade.dev
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cade-cli"
3
- version = "0.15.8"
3
+ version = "0.15.9"
4
4
  description = "Cade - The CLI Agent from Arcade.dev"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.[/dim]"
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 messages from
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 and self.settings.verify_delivery:
256
- self._spawn_background(self._verify_delivery(guid), name="imessage-verify")
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
- sender_raw = str(message.get("sender") or "")
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=canonical_handle(sender_raw, self.settings.region or "US"),
534
- sender_name=str(message.get("sender_name") or sender_raw or "unknown"),
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