crewly 1.20.122 → 1.20.124

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 (225) hide show
  1. package/config/skills/agent/core/whatsapp-draft/SKILL.md +80 -0
  2. package/config/skills/agent/core/whatsapp-draft/execute.sh +84 -0
  3. package/config/skills/agent/core/whatsapp-draft/execute.test.sh +70 -0
  4. package/config/skills/agent/core/whatsapp-inbox/SKILL.md +75 -0
  5. package/config/skills/agent/core/whatsapp-inbox/execute.sh +87 -0
  6. package/config/skills/agent/core/whatsapp-inbox/execute.test.sh +68 -0
  7. package/config/skills/agent/core/whatsapp-read/SKILL.md +80 -0
  8. package/config/skills/agent/core/whatsapp-read/execute.sh +116 -0
  9. package/config/skills/agent/core/whatsapp-read/execute.test.sh +84 -0
  10. package/config/skills/agent/core/whatsapp-send/SKILL.md +70 -0
  11. package/config/skills/agent/core/whatsapp-send/execute.sh +84 -0
  12. package/config/skills/agent/core/whatsapp-send/execute.test.sh +79 -0
  13. package/dist/backend/backend/src/constants.d.ts +225 -0
  14. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  15. package/dist/backend/backend/src/constants.js +223 -0
  16. package/dist/backend/backend/src/constants.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/harness/harness.controller.d.ts +54 -0
  18. package/dist/backend/backend/src/controllers/harness/harness.controller.d.ts.map +1 -0
  19. package/dist/backend/backend/src/controllers/harness/harness.controller.js +204 -0
  20. package/dist/backend/backend/src/controllers/harness/harness.controller.js.map +1 -0
  21. package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts +27 -0
  22. package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts.map +1 -0
  23. package/dist/backend/backend/src/controllers/harness/harness.routes.js +43 -0
  24. package/dist/backend/backend/src/controllers/harness/harness.routes.js.map +1 -0
  25. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts +73 -0
  26. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts.map +1 -0
  27. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js +310 -0
  28. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js.map +1 -0
  29. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -1
  30. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +53 -4
  31. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -1
  32. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  33. package/dist/backend/backend/src/routes/api.routes.js +4 -0
  34. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  35. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +5 -0
  36. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/agent/agent-registration.service.js +7 -0
  38. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  39. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
  41. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  42. package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts +2 -1
  43. package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/cloud/cloud-client.service.js +4 -3
  45. package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
  46. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +43 -1
  48. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  49. package/dist/backend/backend/src/services/harness/claude-config.utils.d.ts +67 -0
  50. package/dist/backend/backend/src/services/harness/claude-config.utils.d.ts.map +1 -0
  51. package/dist/backend/backend/src/services/harness/claude-config.utils.js +118 -0
  52. package/dist/backend/backend/src/services/harness/claude-config.utils.js.map +1 -0
  53. package/dist/backend/backend/src/services/harness/harness-api-key.service.d.ts +99 -0
  54. package/dist/backend/backend/src/services/harness/harness-api-key.service.d.ts.map +1 -0
  55. package/dist/backend/backend/src/services/harness/harness-api-key.service.js +157 -0
  56. package/dist/backend/backend/src/services/harness/harness-api-key.service.js.map +1 -0
  57. package/dist/backend/backend/src/services/harness/harness-credentials.store.d.ts +135 -0
  58. package/dist/backend/backend/src/services/harness/harness-credentials.store.d.ts.map +1 -0
  59. package/dist/backend/backend/src/services/harness/harness-credentials.store.js +208 -0
  60. package/dist/backend/backend/src/services/harness/harness-credentials.store.js.map +1 -0
  61. package/dist/backend/backend/src/services/harness/harness-exec.utils.d.ts +85 -0
  62. package/dist/backend/backend/src/services/harness/harness-exec.utils.d.ts.map +1 -0
  63. package/dist/backend/backend/src/services/harness/harness-exec.utils.js +183 -0
  64. package/dist/backend/backend/src/services/harness/harness-exec.utils.js.map +1 -0
  65. package/dist/backend/backend/src/services/harness/harness-install.service.d.ts +106 -0
  66. package/dist/backend/backend/src/services/harness/harness-install.service.d.ts.map +1 -0
  67. package/dist/backend/backend/src/services/harness/harness-install.service.js +205 -0
  68. package/dist/backend/backend/src/services/harness/harness-install.service.js.map +1 -0
  69. package/dist/backend/backend/src/services/harness/harness-registry.d.ts +86 -0
  70. package/dist/backend/backend/src/services/harness/harness-registry.d.ts.map +1 -0
  71. package/dist/backend/backend/src/services/harness/harness-registry.js +121 -0
  72. package/dist/backend/backend/src/services/harness/harness-registry.js.map +1 -0
  73. package/dist/backend/backend/src/services/harness/harness-status.service.d.ts +173 -0
  74. package/dist/backend/backend/src/services/harness/harness-status.service.d.ts.map +1 -0
  75. package/dist/backend/backend/src/services/harness/harness-status.service.js +324 -0
  76. package/dist/backend/backend/src/services/harness/harness-status.service.js.map +1 -0
  77. package/dist/backend/backend/src/services/harness/harness.service.d.ts +146 -0
  78. package/dist/backend/backend/src/services/harness/harness.service.d.ts.map +1 -0
  79. package/dist/backend/backend/src/services/harness/harness.service.js +193 -0
  80. package/dist/backend/backend/src/services/harness/harness.service.js.map +1 -0
  81. package/dist/backend/backend/src/services/harness/harness.types.d.ts +138 -0
  82. package/dist/backend/backend/src/services/harness/harness.types.d.ts.map +1 -0
  83. package/dist/backend/backend/src/services/harness/harness.types.js +51 -0
  84. package/dist/backend/backend/src/services/harness/harness.types.js.map +1 -0
  85. package/dist/backend/backend/src/services/harness/login-broker.service.d.ts +238 -0
  86. package/dist/backend/backend/src/services/harness/login-broker.service.d.ts.map +1 -0
  87. package/dist/backend/backend/src/services/harness/login-broker.service.js +495 -0
  88. package/dist/backend/backend/src/services/harness/login-broker.service.js.map +1 -0
  89. package/dist/backend/backend/src/services/harness/login-rules.d.ts +162 -0
  90. package/dist/backend/backend/src/services/harness/login-rules.d.ts.map +1 -0
  91. package/dist/backend/backend/src/services/harness/login-rules.js +344 -0
  92. package/dist/backend/backend/src/services/harness/login-rules.js.map +1 -0
  93. package/dist/backend/backend/src/services/harness/orc-harness.store.d.ts +59 -0
  94. package/dist/backend/backend/src/services/harness/orc-harness.store.d.ts.map +1 -0
  95. package/dist/backend/backend/src/services/harness/orc-harness.store.js +108 -0
  96. package/dist/backend/backend/src/services/harness/orc-harness.store.js.map +1 -0
  97. package/dist/backend/backend/src/services/whatsapp/index.d.ts +5 -1
  98. package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -1
  99. package/dist/backend/backend/src/services/whatsapp/index.js +5 -1
  100. package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -1
  101. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts +59 -0
  102. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts.map +1 -0
  103. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js +108 -0
  104. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js.map +1 -0
  105. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts +74 -0
  106. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts.map +1 -0
  107. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js +82 -0
  108. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js.map +1 -0
  109. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts +184 -0
  110. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts.map +1 -0
  111. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js +420 -0
  112. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js.map +1 -0
  113. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts +243 -0
  114. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts.map +1 -0
  115. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js +538 -0
  116. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js.map +1 -0
  117. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +32 -2
  118. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -1
  119. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +65 -9
  120. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -1
  121. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
  122. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +11 -0
  123. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -1
  124. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +59 -1
  125. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -1
  126. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +159 -9
  127. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -1
  128. package/dist/backend/backend/src/types/whatsapp.types.d.ts +108 -0
  129. package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -1
  130. package/dist/backend/backend/src/types/whatsapp.types.js +9 -0
  131. package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -1
  132. package/dist/backend/build-info.json +2 -2
  133. package/dist/cli/backend/src/constants.d.ts +225 -0
  134. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  135. package/dist/cli/backend/src/constants.js +223 -0
  136. package/dist/cli/backend/src/constants.js.map +1 -1
  137. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +5 -0
  138. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  139. package/dist/cli/backend/src/services/agent/agent-registration.service.js +7 -0
  140. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  141. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  142. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
  143. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  144. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +2 -1
  145. package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
  146. package/dist/cli/backend/src/services/cloud/cloud-client.service.js +4 -3
  147. package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -1
  148. package/dist/cli/backend/src/services/harness/claude-config.utils.d.ts +67 -0
  149. package/dist/cli/backend/src/services/harness/claude-config.utils.d.ts.map +1 -0
  150. package/dist/cli/backend/src/services/harness/claude-config.utils.js +118 -0
  151. package/dist/cli/backend/src/services/harness/claude-config.utils.js.map +1 -0
  152. package/dist/cli/backend/src/services/harness/harness-api-key.service.d.ts +99 -0
  153. package/dist/cli/backend/src/services/harness/harness-api-key.service.d.ts.map +1 -0
  154. package/dist/cli/backend/src/services/harness/harness-api-key.service.js +157 -0
  155. package/dist/cli/backend/src/services/harness/harness-api-key.service.js.map +1 -0
  156. package/dist/cli/backend/src/services/harness/harness-credentials.store.d.ts +135 -0
  157. package/dist/cli/backend/src/services/harness/harness-credentials.store.d.ts.map +1 -0
  158. package/dist/cli/backend/src/services/harness/harness-credentials.store.js +208 -0
  159. package/dist/cli/backend/src/services/harness/harness-credentials.store.js.map +1 -0
  160. package/dist/cli/backend/src/services/harness/harness-exec.utils.d.ts +85 -0
  161. package/dist/cli/backend/src/services/harness/harness-exec.utils.d.ts.map +1 -0
  162. package/dist/cli/backend/src/services/harness/harness-exec.utils.js +183 -0
  163. package/dist/cli/backend/src/services/harness/harness-exec.utils.js.map +1 -0
  164. package/dist/cli/backend/src/services/harness/harness-install.service.d.ts +106 -0
  165. package/dist/cli/backend/src/services/harness/harness-install.service.d.ts.map +1 -0
  166. package/dist/cli/backend/src/services/harness/harness-install.service.js +205 -0
  167. package/dist/cli/backend/src/services/harness/harness-install.service.js.map +1 -0
  168. package/dist/cli/backend/src/services/harness/harness-registry.d.ts +86 -0
  169. package/dist/cli/backend/src/services/harness/harness-registry.d.ts.map +1 -0
  170. package/dist/cli/backend/src/services/harness/harness-registry.js +121 -0
  171. package/dist/cli/backend/src/services/harness/harness-registry.js.map +1 -0
  172. package/dist/cli/backend/src/services/harness/harness-status.service.d.ts +173 -0
  173. package/dist/cli/backend/src/services/harness/harness-status.service.d.ts.map +1 -0
  174. package/dist/cli/backend/src/services/harness/harness-status.service.js +324 -0
  175. package/dist/cli/backend/src/services/harness/harness-status.service.js.map +1 -0
  176. package/dist/cli/backend/src/services/harness/harness.service.d.ts +146 -0
  177. package/dist/cli/backend/src/services/harness/harness.service.d.ts.map +1 -0
  178. package/dist/cli/backend/src/services/harness/harness.service.js +193 -0
  179. package/dist/cli/backend/src/services/harness/harness.service.js.map +1 -0
  180. package/dist/cli/backend/src/services/harness/harness.types.d.ts +138 -0
  181. package/dist/cli/backend/src/services/harness/harness.types.d.ts.map +1 -0
  182. package/dist/cli/backend/src/services/harness/harness.types.js +51 -0
  183. package/dist/cli/backend/src/services/harness/harness.types.js.map +1 -0
  184. package/dist/cli/backend/src/services/harness/login-broker.service.d.ts +238 -0
  185. package/dist/cli/backend/src/services/harness/login-broker.service.d.ts.map +1 -0
  186. package/dist/cli/backend/src/services/harness/login-broker.service.js +495 -0
  187. package/dist/cli/backend/src/services/harness/login-broker.service.js.map +1 -0
  188. package/dist/cli/backend/src/services/harness/login-rules.d.ts +162 -0
  189. package/dist/cli/backend/src/services/harness/login-rules.d.ts.map +1 -0
  190. package/dist/cli/backend/src/services/harness/login-rules.js +344 -0
  191. package/dist/cli/backend/src/services/harness/login-rules.js.map +1 -0
  192. package/dist/cli/backend/src/services/harness/orc-harness.store.d.ts +59 -0
  193. package/dist/cli/backend/src/services/harness/orc-harness.store.d.ts.map +1 -0
  194. package/dist/cli/backend/src/services/harness/orc-harness.store.js +108 -0
  195. package/dist/cli/backend/src/services/harness/orc-harness.store.js.map +1 -0
  196. package/dist/cli/cli/src/commands/harness-setup.d.ts +159 -0
  197. package/dist/cli/cli/src/commands/harness-setup.d.ts.map +1 -0
  198. package/dist/cli/cli/src/commands/harness-setup.js +377 -0
  199. package/dist/cli/cli/src/commands/harness-setup.js.map +1 -0
  200. package/dist/cli/cli/src/commands/harness.d.ts +56 -0
  201. package/dist/cli/cli/src/commands/harness.d.ts.map +1 -0
  202. package/dist/cli/cli/src/commands/harness.js +97 -0
  203. package/dist/cli/cli/src/commands/harness.js.map +1 -0
  204. package/dist/cli/cli/src/commands/onboard.d.ts +108 -50
  205. package/dist/cli/cli/src/commands/onboard.d.ts.map +1 -1
  206. package/dist/cli/cli/src/commands/onboard.js +200 -191
  207. package/dist/cli/cli/src/commands/onboard.js.map +1 -1
  208. package/dist/cli/cli/src/constants.d.ts +18 -0
  209. package/dist/cli/cli/src/constants.d.ts.map +1 -1
  210. package/dist/cli/cli/src/constants.js +18 -0
  211. package/dist/cli/cli/src/constants.js.map +1 -1
  212. package/dist/cli/cli/src/index.js +22 -3
  213. package/dist/cli/cli/src/index.js.map +1 -1
  214. package/dist/cli/cli/src/utils/harness-engine.d.ts +99 -0
  215. package/dist/cli/cli/src/utils/harness-engine.d.ts.map +1 -0
  216. package/dist/cli/cli/src/utils/harness-engine.js +143 -0
  217. package/dist/cli/cli/src/utils/harness-engine.js.map +1 -0
  218. package/dist/cli/cli/src/utils/prompt-io.d.ts +25 -0
  219. package/dist/cli/cli/src/utils/prompt-io.d.ts.map +1 -0
  220. package/dist/cli/cli/src/utils/prompt-io.js +47 -0
  221. package/dist/cli/cli/src/utils/prompt-io.js.map +1 -0
  222. package/frontend/dist/assets/{index-8327e63e.css → index-0a2f783c.css} +1 -1
  223. package/frontend/dist/assets/{index-07e338a9.js → index-aaf65055.js} +309 -309
  224. package/frontend/dist/index.html +2 -2
  225. package/package.json +1 -1
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: WhatsApp Draft
3
+ description: Write a reply draft for one of the owner's WhatsApp chats. It is saved with a short code (W12) and NEVER sent — the owner releases it by replying 「发 W12」 or from the dashboard.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - generalist
11
+ - operations
12
+ - ops
13
+ - support
14
+ - sales
15
+ triggers:
16
+ - draft whatsapp reply
17
+ - reply on whatsapp
18
+ - write a whatsapp message
19
+ tags:
20
+ - whatsapp
21
+ - draft
22
+ execution:
23
+ type: script
24
+ script:
25
+ file: execute.sh
26
+ interpreter: bash
27
+ timeoutMs: 30000
28
+ ---
29
+
30
+ # WhatsApp Draft
31
+
32
+ Saves a proposed reply. **It never sends.**
33
+
34
+ ```bash
35
+ bash execute.sh --chat 4915550001@s.whatsapp.net --text "Yes, 8 works — see you there"
36
+ bash execute.sh --chat 4915550001@s.whatsapp.net --text-file /tmp/reply.txt
37
+ bash execute.sh '{"chatId":"4915550001@s.whatsapp.net","text":"…"}'
38
+ ```
39
+
40
+ ## After drafting — what you must do
41
+
42
+ Show the owner, in your reply to them:
43
+
44
+ 1. **who** it goes to (`draft.recipient`),
45
+ 2. **the exact text** (`draft.text`),
46
+ 3. **the code** (`draft.code`, e.g. `W12`),
47
+
48
+ and ask them to reply **「发 W12」** to send it (or 发送 / 丢弃 on the dashboard's
49
+ WhatsApp tab). Then stop. Only after they have replied 「发 W12」 may you run
50
+ `whatsapp-send --draft W12`. If they change the wording, write a new draft.
51
+
52
+ ## Rules (owner decision — apply to every whatsapp-* skill)
53
+
54
+ - **Never send anything without the owner's explicit 「发 <code>」.** One draft,
55
+ one confirmation, one message.
56
+ - **Never auto-reply to anyone** — no acknowledgements, no "will get back to you".
57
+ - **Summarise; don't paste private chats wholesale** into other channels. The
58
+ owner needs the gist and your proposed reply, not a transcript.
59
+ - **Groups are low priority.** Draft for a group only when the owner asks.
60
+
61
+ ## Parameters
62
+
63
+ | Name | Required | Meaning |
64
+ |---|---|---|
65
+ | `--chat` (`chatId`) | yes | Recipient chat JID; must already be in the inbox |
66
+ | `--text` (`text`) | yes* | Reply text (max 4000 chars) |
67
+ | `--text-file` | yes* | Read the text from a file instead |
68
+
69
+ ## Output
70
+
71
+ ```json
72
+ {"success":true,"sent":false,
73
+ "draft":{"id":"…","code":"W12","recipient":"Ann","chatId":"4915550001@s.whatsapp.net","text":"Yes, 8 works — see you there"},
74
+ "nextStep":"NOT SENT. Show the owner: recipient Ann, the exact text above, and code W12. Ask them to reply 「发 W12」 …"}
75
+ ```
76
+
77
+ ## Failures
78
+
79
+ `{"success":false,"reason":"chat_not_found"}` (exit 1) — you can only draft to a
80
+ chat that is in the inbox; find it with `whatsapp-read --chats --q <name>`.
@@ -0,0 +1,84 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # whatsapp-draft — Propose a reply to a WhatsApp chat. NEVER sends.
4
+ #
5
+ # Backed by POST /api/whatsapp/drafts {chatId, text}. The draft gets a short
6
+ # code (W12). Only the owner releases it: by replying 「发 W12」 in chat (then
7
+ # whatsapp-send), or by pressing 发送 in the dashboard.
8
+ #
9
+ # Usage:
10
+ # bash execute.sh --chat <jid> --text "reply text"
11
+ # bash execute.sh --chat <jid> --text-file /path/to/reply.txt
12
+ # bash execute.sh '{"chatId":"…","text":"…"}'
13
+ # =============================================================================
14
+ set -euo pipefail
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
17
+
18
+ print_usage() {
19
+ cat <<'EOF_USAGE'
20
+ Usage:
21
+ bash execute.sh --chat <jid> --text "reply text"
22
+ bash execute.sh --chat <jid> --text-file /path/to/reply.txt
23
+ bash execute.sh '{"chatId":"…","text":"…"}'
24
+
25
+ Options:
26
+ --chat Recipient chat JID (must already be in the inbox)
27
+ --text Reply text
28
+ --text-file Read the reply text from a file
29
+ --help | -h Show this help
30
+
31
+ This only saves a draft. It never sends.
32
+ EOF_USAGE
33
+ }
34
+
35
+ INPUT_JSON=""
36
+ CHAT=""; TEXT=""; TEXT_FILE=""
37
+
38
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
39
+ INPUT_JSON="$1"
40
+ shift || true
41
+ fi
42
+
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --chat|-c) [ $# -ge 2 ] || error_exit "--chat requires a value"; CHAT="$2"; shift 2 ;;
46
+ --text) [ $# -ge 2 ] || error_exit "--text requires a value"; TEXT="$2"; shift 2 ;;
47
+ --text-file) [ $# -ge 2 ] || error_exit "--text-file requires a value"; TEXT_FILE="$2"; shift 2 ;;
48
+ --help|-h) print_usage; exit 0 ;;
49
+ *) error_exit "Unknown option: $1" ;;
50
+ esac
51
+ done
52
+
53
+ if [ -n "$INPUT_JSON" ]; then
54
+ INPUT=$(read_json_input "$INPUT_JSON")
55
+ [ -z "$CHAT" ] && CHAT=$(printf '%s' "$INPUT" | jq -r '.chatId // .chat // empty')
56
+ [ -z "$TEXT" ] && TEXT=$(printf '%s' "$INPUT" | jq -r '.text // empty')
57
+ fi
58
+
59
+ if [ -n "$TEXT_FILE" ]; then
60
+ [ -f "$TEXT_FILE" ] || error_exit "text file not found: $TEXT_FILE"
61
+ TEXT=$(cat "$TEXT_FILE")
62
+ fi
63
+
64
+ require_param "chat (--chat)" "$CHAT"
65
+ require_param "text (--text or --text-file)" "$TEXT"
66
+
67
+ BODY=$(jq -cn --arg chatId "$CHAT" --arg text "$TEXT" '{chatId: $chatId, text: $text}')
68
+
69
+ # stderr is kept apart so lib.sh's session warning cannot pollute a good response.
70
+ ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
71
+ RESPONSE=$(api_call POST "/whatsapp/drafts" "$BODY" 2>"$ERR_FILE") || {
72
+ ERR=$(tail -n 1 "$ERR_FILE")
73
+ printf '%s' "$ERR" | jq -c '{success: false, reason: (.details.code // .details.error // .details // .error // "unknown"), message: (.details.error // "")}' 2>/dev/null \
74
+ || jq -n --arg r "$ERR" '{success: false, reason: $r}'
75
+ exit 1
76
+ }
77
+
78
+ # The next step is the owner's, not ours: say so in the output itself.
79
+ printf '%s' "$RESPONSE" | jq -c '.data as $d | {
80
+ success: true,
81
+ sent: false,
82
+ draft: {id: $d.id, code: $d.code, recipient: $d.recipient, chatId: $d.chatId, text: $d.text},
83
+ nextStep: ("NOT SENT. Show the owner: recipient \($d.recipient), the exact text above, and code \($d.code). Ask them to reply 「发 \($d.code)」 to send it (or use 发送/丢弃 in the dashboard). Do not call whatsapp-send until they have replied 「发 \($d.code)」.")
84
+ }'
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # Tests for whatsapp-draft — run with: bash execute.test.sh </dev/null
3
+ # Spins up a python HTTP stub as the backend, asserts the request body the
4
+ # skill sends and the JSON it prints. Exit 0 on pass.
5
+ set -u
6
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ EXEC="$HERE/execute.sh"
8
+ PASS=0; FAIL=0
9
+ check() {
10
+ local name="$1"; local got="$2"; local want="$3"
11
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
12
+ }
13
+
14
+ PORT=18843
15
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
16
+ python3 -u <<'PY' >/dev/null 2>&1 &
17
+ from http.server import BaseHTTPRequestHandler, HTTPServer
18
+ import json, os
19
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
20
+ class H(BaseHTTPRequestHandler):
21
+ def reply(self, code, body):
22
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
23
+ self.wfile.write(json.dumps(body).encode())
24
+ def do_POST(self):
25
+ body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))) or b'{}')
26
+ open(LOG, 'w').write(json.dumps({'method': 'POST', 'path': self.path, 'body': body, 'agent': self.headers.get('X-Agent-Session')}))
27
+ if body.get('chatId') == 'unknown@s.whatsapp.net':
28
+ return self.reply(404, {'success': False, 'code': 'chat_not_found', 'error': 'Unknown chat: unknown@s.whatsapp.net'})
29
+ self.reply(201, {'success': True, 'data': {'id': 'd-1', 'code': 'W12', 'seq': 12, 'chatId': body['chatId'], 'text': body['text'],
30
+ 'status': 'pending', 'recipient': 'Ann', 'instruction': 'server says'}})
31
+ def log_message(self, *a, **k): pass
32
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
33
+ PY
34
+ STUB_PID=$!
35
+ disown "$STUB_PID" 2>/dev/null || true
36
+ TMP_TEXT="$(mktemp)"
37
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG" "$TMP_TEXT"' EXIT
38
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
39
+
40
+ run() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
41
+ run_err() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
42
+
43
+ OUT=$(run --chat 491@s.whatsapp.net --text 'Yes, 8 works — see you "there"')
44
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/drafts"
45
+ check "flags: body" "$(jq -c .body "$STUB_LOG")" '{"chatId":"491@s.whatsapp.net","text":"Yes, 8 works — see you \"there\""}'
46
+ check "flags: agent header" "$(jq -r .agent "$STUB_LOG")" "test-agent"
47
+ check "output: not sent" "$(printf '%s' "$OUT" | jq -r .sent)" "false"
48
+ check "output: draft" "$(printf '%s' "$OUT" | jq -c .draft)" '{"id":"d-1","code":"W12","recipient":"Ann","chatId":"491@s.whatsapp.net","text":"Yes, 8 works — see you \"there\""}'
49
+ NEXT=$(printf '%s' "$OUT" | jq -r .nextStep)
50
+ [[ "$NEXT" == *"NOT SENT"* && "$NEXT" == *"Ann"* && "$NEXT" == *"「发 W12」"* ]] && OK=yes || OK=no
51
+ check "output: nextStep tells the agent to ask the owner for 「发 W12」" "$OK" "yes"
52
+
53
+ printf 'line one\nline two' > "$TMP_TEXT"
54
+ run --chat 491@s.whatsapp.net --text-file "$TMP_TEXT" >/dev/null
55
+ check "text-file: body text" "$(jq -r .body.text "$STUB_LOG")" "$(printf 'line one\nline two')"
56
+
57
+ run '{"chatId":"491@s.whatsapp.net","text":"json hi"}' >/dev/null
58
+ check "json: body" "$(jq -c .body "$STUB_LOG")" '{"chatId":"491@s.whatsapp.net","text":"json hi"}'
59
+
60
+ OUT=$(CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --chat unknown@s.whatsapp.net --text hi 2>/dev/null); RC=$?
61
+ check "404: exit code" "$RC" "1"
62
+ check "404: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "chat_not_found"
63
+
64
+ check "missing chat" "$(run_err --text hi)" "Missing required parameter: chat (--chat)"
65
+ check "missing text" "$(run_err --chat 491@s.whatsapp.net)" "Missing required parameter: text (--text or --text-file)"
66
+ check "missing text file" "$(run_err --chat x --text-file /nonexistent/file)" "text file not found: /nonexistent/file"
67
+ check "unknown option errors" "$(run_err --send)" "Unknown option: --send"
68
+
69
+ echo "=== Results: $PASS passed, $FAIL failed ==="
70
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: WhatsApp Inbox
3
+ description: List the owner's WhatsApp chats that need a reply (last message not from the owner), with how many messages are unanswered. Read-only. 1:1 chats by default; groups are low priority.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - generalist
11
+ - operations
12
+ - ops
13
+ - support
14
+ - sales
15
+ triggers:
16
+ - whatsapp inbox
17
+ - whatsapp messages to reply
18
+ - who needs a reply on whatsapp
19
+ - unread whatsapp
20
+ tags:
21
+ - whatsapp
22
+ - inbox
23
+ execution:
24
+ type: script
25
+ script:
26
+ file: execute.sh
27
+ interpreter: bash
28
+ timeoutMs: 30000
29
+ ---
30
+
31
+ # WhatsApp Inbox
32
+
33
+ Which of the owner's WhatsApp chats are waiting on them.
34
+
35
+ ```bash
36
+ bash execute.sh # 1:1 chats needing a reply, newest first
37
+ bash execute.sh --limit 50
38
+ bash execute.sh --include-groups # groups too (noisy, low priority)
39
+ bash execute.sh '{"limit":20,"includeGroups":true}'
40
+ ```
41
+
42
+ ## Rules (owner decision — apply to every whatsapp-* skill)
43
+
44
+ - **Never send anything without the owner's explicit 「发 <code>」.** You may only
45
+ write drafts (`whatsapp-draft`). Sending is one draft at a time, after the owner
46
+ replies 「发 W12」 in chat or presses 发送 in the dashboard.
47
+ - **Never auto-reply to anyone.** Not even "got it" or "will get back to you".
48
+ - **Summarise; don't paste private chats wholesale** into Slack, team channels,
49
+ the wiki, tasks, or any other place. Quote only the line you need.
50
+ - **Groups are low priority.** Look at 1:1 chats first; only surface a group
51
+ when the owner is addressed directly or something is clearly urgent.
52
+
53
+ ## Parameters
54
+
55
+ | Name | Required | Meaning |
56
+ |---|---|---|
57
+ | `--limit` (`limit`) | no | Max chats, default 20, max 200 |
58
+ | `--include-groups` (`includeGroups`) | no | Include group chats (default off) |
59
+
60
+ ## Output
61
+
62
+ ```json
63
+ {"count":1,"chats":[{"chatId":"4915550001@s.whatsapp.net","name":"Ann","isGroup":false,"unanswered":2,"lastKind":"text","lastFrom":"Ann","lastText":"at 8?","lastAt":"2026-09-24T18:02:11Z"}]}
64
+ ```
65
+
66
+ `unanswered` counts messages from them since the owner last wrote in that chat.
67
+ `lastText` is cut to 200 characters; use `whatsapp-read --chat <chatId>` for the
68
+ conversation. `lastKind` is `text`, `image`, `document`, `audio`, `video`,
69
+ `sticker` or `other` (media is never downloaded — you see the caption only).
70
+
71
+ ## Failures
72
+
73
+ `{"success":false,"reason":"…"}` (exit 1). An empty list with `count: 0` just
74
+ means nothing is waiting — or WhatsApp is not connected in inbox mode yet
75
+ (the owner connects it under Settings → WhatsApp).
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # whatsapp-inbox — Which WhatsApp chats need a reply (read-only)
4
+ #
5
+ # Backed by GET /api/whatsapp/inbox?limit=&includeGroups=.
6
+ # A chat "needs a reply" when its newest message is not the owner's.
7
+ # Groups are excluded unless --include-groups is given (they are noisy).
8
+ #
9
+ # Usage:
10
+ # bash execute.sh [--limit 20] [--include-groups]
11
+ # bash execute.sh '{"limit":20,"includeGroups":true}'
12
+ # =============================================================================
13
+ set -euo pipefail
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
16
+
17
+ # Characters of each chat's last message shown (keeps the output small).
18
+ PREVIEW_CHARS="${CREWLY_WHATSAPP_PREVIEW_CHARS:-200}"
19
+
20
+ print_usage() {
21
+ cat <<'EOF_USAGE'
22
+ Usage:
23
+ bash execute.sh [--limit 20] [--include-groups]
24
+ bash execute.sh '{"limit":20,"includeGroups":true}'
25
+
26
+ Options:
27
+ --limit Max chats (default 20, max 200)
28
+ --include-groups Also list group chats (default: 1:1 chats only)
29
+ --help | -h Show this help
30
+ EOF_USAGE
31
+ }
32
+
33
+ INPUT_JSON=""
34
+ LIMIT=""; INCLUDE_GROUPS=""
35
+
36
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
37
+ INPUT_JSON="$1"
38
+ shift || true
39
+ fi
40
+
41
+ while [[ $# -gt 0 ]]; do
42
+ case "$1" in
43
+ --limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
44
+ --include-groups) INCLUDE_GROUPS=1; shift ;;
45
+ --help|-h) print_usage; exit 0 ;;
46
+ *) error_exit "Unknown option: $1" ;;
47
+ esac
48
+ done
49
+
50
+ if [ -n "$INPUT_JSON" ]; then
51
+ INPUT=$(read_json_input "$INPUT_JSON")
52
+ [ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
53
+ [ -z "$INCLUDE_GROUPS" ] && INCLUDE_GROUPS=$(printf '%s' "$INPUT" | jq -r 'if .includeGroups == true then "1" else "" end')
54
+ fi
55
+
56
+ QS=""
57
+ [ -n "$LIMIT" ] && QS="limit=$(jq -rn --arg v "$LIMIT" '$v|@uri')"
58
+ if [ -n "$INCLUDE_GROUPS" ]; then
59
+ QS="${QS:+${QS}&}includeGroups=true"
60
+ fi
61
+
62
+ # stderr is kept apart so lib.sh's session warning cannot pollute a good response.
63
+ ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
64
+ RESPONSE=$(api_call GET "/whatsapp/inbox${QS:+?${QS}}" 2>"$ERR_FILE") || {
65
+ ERR=$(tail -n 1 "$ERR_FILE")
66
+ printf '%s' "$ERR" | jq -c '{success: false, reason: (.details.code // .details.error // .details // .error // "unknown"), message: (.details.error // "")}' 2>/dev/null \
67
+ || jq -n --arg r "$ERR" '{success: false, reason: $r}'
68
+ exit 1
69
+ }
70
+
71
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
72
+ printf '%s\n' "$RESPONSE"; exit 0
73
+ fi
74
+
75
+ printf '%s' "$RESPONSE" | jq -c --argjson n "$PREVIEW_CHARS" '{
76
+ count: (.data | length),
77
+ chats: [.data[] | {
78
+ chatId: .chat.id,
79
+ name: (.chat.name // .chat.id),
80
+ isGroup: .chat.isGroup,
81
+ unanswered: .unansweredCount,
82
+ lastKind: .lastKind,
83
+ lastFrom: .lastSenderName,
84
+ lastText: (.lastText | .[0:$n]),
85
+ lastAt: ((.lastMessageAt / 1000) | floor | todate)
86
+ }]
87
+ }'
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+ # Tests for whatsapp-inbox — run with: bash execute.test.sh </dev/null
3
+ # Spins up a python HTTP stub as the backend, asserts the request path the
4
+ # skill sends and the JSON it prints. Exit 0 on pass.
5
+ set -u
6
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ EXEC="$HERE/execute.sh"
8
+ PASS=0; FAIL=0
9
+ check() {
10
+ local name="$1"; local got="$2"; local want="$3"
11
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
12
+ }
13
+
14
+ PORT=18841
15
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
16
+ python3 -u <<'PY' >/dev/null 2>&1 &
17
+ from http.server import BaseHTTPRequestHandler, HTTPServer
18
+ from urllib.parse import urlparse, parse_qs
19
+ import json, os
20
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
21
+ ENTRY = {'chat': {'id': '491@s.whatsapp.net', 'name': 'Ann', 'isGroup': False, 'lastMessageAt': 1760000000000},
22
+ 'unansweredCount': 2, 'lastText': 'x' * 300, 'lastKind': 'text', 'lastSenderName': 'Ann', 'lastMessageAt': 1760000000000}
23
+ class H(BaseHTTPRequestHandler):
24
+ def do_GET(self):
25
+ open(LOG, 'w').write(json.dumps({'method': 'GET', 'path': self.path, 'agent': self.headers.get('X-Agent-Session')}))
26
+ qs = parse_qs(urlparse(self.path).query)
27
+ if qs.get('limit', [''])[0] == '999':
28
+ self.send_response(500); self.send_header('Content-Type', 'application/json'); self.end_headers()
29
+ self.wfile.write(json.dumps({'success': False, 'error': 'db locked'}).encode()); return
30
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
31
+ self.wfile.write(json.dumps({'success': True, 'data': [ENTRY]}).encode())
32
+ def log_message(self, *a, **k): pass
33
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
34
+ PY
35
+ STUB_PID=$!
36
+ disown "$STUB_PID" 2>/dev/null || true
37
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
38
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
39
+
40
+ run() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
41
+ run_err() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
42
+
43
+ X200=$(printf 'x%.0s' $(seq 1 200))
44
+ WANT='{"count":1,"chats":[{"chatId":"491@s.whatsapp.net","name":"Ann","isGroup":false,"unanswered":2,"lastKind":"text","lastFrom":"Ann","lastText":"'"$X200"'","lastAt":"2025-10-09T08:53:20Z"}]}'
45
+
46
+ OUT=$(run)
47
+ check "defaults: output (preview truncated to 200)" "$OUT" "$WANT"
48
+ check "defaults: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox"
49
+ check "sends the agent session header" "$(jq -r .agent "$STUB_LOG")" "test-agent"
50
+
51
+ run --limit 5 --include-groups >/dev/null
52
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox?limit=5&includeGroups=true"
53
+
54
+ run '{"limit":7,"includeGroups":true}' >/dev/null
55
+ check "json: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox?limit=7&includeGroups=true"
56
+
57
+ run --include-groups >/dev/null
58
+ check "groups only: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox?includeGroups=true"
59
+
60
+ OUT=$(CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --limit 999 2>/dev/null); RC=$?
61
+ check "500: exit code" "$RC" "1"
62
+ check "500: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "db locked"
63
+
64
+ check "unknown option errors" "$(run_err --bogus)" "Unknown option: --bogus"
65
+ check "--limit needs a value" "$(run_err --limit)" "--limit requires a value"
66
+
67
+ echo "=== Results: $PASS passed, $FAIL failed ==="
68
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: WhatsApp Read
3
+ description: Read one of the owner's WhatsApp chats, search their WhatsApp messages, or list recent chats. Read-only. For summarising and preparing replies — never for forwarding private chats elsewhere.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - generalist
11
+ - operations
12
+ - ops
13
+ - support
14
+ - sales
15
+ triggers:
16
+ - read whatsapp chat
17
+ - search whatsapp
18
+ - what did they say on whatsapp
19
+ - whatsapp conversation
20
+ tags:
21
+ - whatsapp
22
+ - inbox
23
+ execution:
24
+ type: script
25
+ script:
26
+ file: execute.sh
27
+ interpreter: bash
28
+ timeoutMs: 30000
29
+ ---
30
+
31
+ # WhatsApp Read
32
+
33
+ ```bash
34
+ bash execute.sh --chat 4915550001@s.whatsapp.net # newest 50 messages, oldest first
35
+ bash execute.sh --chat 4915550001@s.whatsapp.net --before 1760000000000 # page back (use nextBefore)
36
+ bash execute.sh --q "invoice" # search all chats
37
+ bash execute.sh --chats --q "ann" # find a chat by name
38
+ ```
39
+
40
+ ## Rules (owner decision — apply to every whatsapp-* skill)
41
+
42
+ - **Never send anything without the owner's explicit 「发 <code>」.** Reading is
43
+ all this skill does; replies go through `whatsapp-draft`, and only the owner
44
+ releases a draft.
45
+ - **Never auto-reply to anyone.**
46
+ - **Summarise; don't paste private chats wholesale** into Slack, team channels,
47
+ the wiki, tasks, memory or any other place. Tell the owner what a chat is
48
+ about and what is being asked; quote only the line you need.
49
+ - **Groups are low priority.** Skim them only when asked or when the owner is
50
+ addressed directly.
51
+
52
+ ## Parameters
53
+
54
+ | Name | Required | Meaning |
55
+ |---|---|---|
56
+ | `--chat` (`chatId`) | one of three | Chat JID (from `whatsapp-inbox` or `--chats`) |
57
+ | `--q` (`q`) | one of three | Search text; with `--chats`, filters chat names |
58
+ | `--chats` (`chats: true`) | one of three | List recent chats |
59
+ | `--limit` (`limit`) | no | Messages default 50 (max 500); search 20 (max 200); chats 50 (max 500) |
60
+ | `--before` (`before`) | no | Epoch ms; only older messages (paging) |
61
+
62
+ ## Output
63
+
64
+ ```json
65
+ {"chat":{"chatId":"4915550001@s.whatsapp.net","name":"Ann","isGroup":false},
66
+ "messages":[{"at":"2026-09-24T18:01:00Z","fromMe":false,"sender":"Ann","kind":"text","text":"at 8?"},
67
+ {"at":"2026-09-24T18:02:00Z","fromMe":true,"sender":"me","kind":"text","text":"yes"}],
68
+ "nextBefore":null}
69
+ ```
70
+
71
+ `fromMe: true` / `sender: "me"` is the owner. Media shows as its `kind`
72
+ (`image`, `document`, `audio`, `video`, `sticker`, `other`) with the caption or
73
+ file name as `text` — files are never downloaded. `nextBefore` is non-null when
74
+ there may be older messages; pass it as `--before`.
75
+
76
+ Search: `{"count":1,"hits":[{"chatId":"…","chatName":"Ann","at":"…","fromMe":false,"sender":"Ann","kind":"text","text":"…"}]}`
77
+
78
+ ## Failures
79
+
80
+ `{"success":false,"reason":"chat_not_found","message":"…"}` (exit 1).
@@ -0,0 +1,116 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # whatsapp-read — Read one WhatsApp chat, search messages, or list chats
4
+ #
5
+ # Backed by:
6
+ # GET /api/whatsapp/chats/<chatId>/messages?limit=&before= (--chat)
7
+ # GET /api/whatsapp/search?q=&limit= (--q)
8
+ # GET /api/whatsapp/chats?q=&limit= (--chats)
9
+ # Read-only.
10
+ #
11
+ # Usage:
12
+ # bash execute.sh --chat <jid> [--limit 50] [--before <epoch ms>]
13
+ # bash execute.sh --q "dinner" [--limit 20]
14
+ # bash execute.sh --chats [--q "ann"] [--limit 50]
15
+ # bash execute.sh '{"chatId":"…","limit":50}' | '{"q":"dinner"}' | '{"chats":true}'
16
+ # =============================================================================
17
+ set -euo pipefail
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
20
+
21
+ print_usage() {
22
+ cat <<'EOF_USAGE'
23
+ Usage:
24
+ bash execute.sh --chat <jid> [--limit 50] [--before <epoch ms>]
25
+ bash execute.sh --q "text" [--limit 20]
26
+ bash execute.sh --chats [--q "name"] [--limit 50]
27
+
28
+ Options:
29
+ --chat Chat JID to read (from whatsapp-inbox / --chats)
30
+ --before Page back: only messages older than this epoch-ms (use nextBefore)
31
+ --q Search text (with --chats: filter chat names)
32
+ --chats List recent chats instead of messages
33
+ --limit Row cap
34
+ --help | -h Show this help
35
+ EOF_USAGE
36
+ }
37
+
38
+ INPUT_JSON=""
39
+ CHAT=""; Q=""; LIMIT=""; BEFORE=""; LIST_CHATS=""
40
+
41
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
42
+ INPUT_JSON="$1"
43
+ shift || true
44
+ fi
45
+
46
+ while [[ $# -gt 0 ]]; do
47
+ case "$1" in
48
+ --chat|-c) [ $# -ge 2 ] || error_exit "--chat requires a value"; CHAT="$2"; shift 2 ;;
49
+ --q|--query) [ $# -ge 2 ] || error_exit "--q requires a value"; Q="$2"; shift 2 ;;
50
+ --limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
51
+ --before) [ $# -ge 2 ] || error_exit "--before requires a value"; BEFORE="$2"; shift 2 ;;
52
+ --chats) LIST_CHATS=1; shift ;;
53
+ --help|-h) print_usage; exit 0 ;;
54
+ *) error_exit "Unknown option: $1" ;;
55
+ esac
56
+ done
57
+
58
+ if [ -n "$INPUT_JSON" ]; then
59
+ INPUT=$(read_json_input "$INPUT_JSON")
60
+ [ -z "$CHAT" ] && CHAT=$(printf '%s' "$INPUT" | jq -r '.chatId // .chat // empty')
61
+ [ -z "$Q" ] && Q=$(printf '%s' "$INPUT" | jq -r '.q // .query // empty')
62
+ [ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
63
+ [ -z "$BEFORE" ] && BEFORE=$(printf '%s' "$INPUT" | jq -r '.before // empty')
64
+ [ -z "$LIST_CHATS" ] && LIST_CHATS=$(printf '%s' "$INPUT" | jq -r 'if .chats == true then "1" else "" end')
65
+ fi
66
+
67
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
68
+
69
+ if [ -n "$LIST_CHATS" ]; then
70
+ MODE=chats
71
+ QS=""
72
+ [ -n "$Q" ] && QS="q=$(uri "$Q")"
73
+ [ -n "$LIMIT" ] && QS="${QS:+${QS}&}limit=$(uri "$LIMIT")"
74
+ ENDPOINT="/whatsapp/chats${QS:+?${QS}}"
75
+ elif [ -n "$CHAT" ]; then
76
+ MODE=chat
77
+ QS=""
78
+ [ -n "$LIMIT" ] && QS="limit=$(uri "$LIMIT")"
79
+ [ -n "$BEFORE" ] && QS="${QS:+${QS}&}before=$(uri "$BEFORE")"
80
+ ENDPOINT="/whatsapp/chats/$(uri "$CHAT")/messages${QS:+?${QS}}"
81
+ elif [ -n "$Q" ]; then
82
+ MODE=search
83
+ QS="q=$(uri "$Q")"
84
+ [ -n "$LIMIT" ] && QS="${QS}&limit=$(uri "$LIMIT")"
85
+ ENDPOINT="/whatsapp/search?${QS}"
86
+ else
87
+ error_exit "Pass --chat <jid>, --q <text>, or --chats"
88
+ fi
89
+
90
+ # stderr is kept apart so lib.sh's session warning cannot pollute a good response.
91
+ ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
92
+ RESPONSE=$(api_call GET "$ENDPOINT" 2>"$ERR_FILE") || {
93
+ ERR=$(tail -n 1 "$ERR_FILE")
94
+ printf '%s' "$ERR" | jq -c '{success: false, reason: (.details.code // .details.error // .details // .error // "unknown"), message: (.details.error // "")}' 2>/dev/null \
95
+ || jq -n --arg r "$ERR" '{success: false, reason: $r}'
96
+ exit 1
97
+ }
98
+
99
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
100
+ printf '%s\n' "$RESPONSE"; exit 0
101
+ fi
102
+
103
+ # One compact line per message: who, when, what.
104
+ MSG_SHAPE='{at: ((.ts / 1000) | floor | todate), fromMe: .fromMe, sender: (if .fromMe then "me" else (.senderName // .senderJid) end), kind: .kind, text: .text}'
105
+
106
+ case "$MODE" in
107
+ chats)
108
+ printf '%s' "$RESPONSE" | jq -c '{count: (.data | length), chats: [.data[] | {chatId: .id, name: (.name // .id), isGroup: .isGroup, lastAt: (if .lastMessageAt then ((.lastMessageAt / 1000) | floor | todate) else null end)}]}'
109
+ ;;
110
+ chat)
111
+ printf '%s' "$RESPONSE" | jq -c "{chat: {chatId: .data.chat.id, name: (.data.chat.name // .data.chat.id), isGroup: .data.chat.isGroup}, messages: [.data.messages[] | ${MSG_SHAPE}], nextBefore: .data.nextBefore}"
112
+ ;;
113
+ search)
114
+ printf '%s' "$RESPONSE" | jq -c "{count: (.data | length), hits: [.data[] | ({chatId: .chatId, chatName: (.chatName // .chatId)} + ${MSG_SHAPE})]}"
115
+ ;;
116
+ esac