crewly 1.16.0 → 1.17.1

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 (200) hide show
  1. package/README.md +33 -1
  2. package/config/roles/orchestrator/prompt.md +20 -5
  3. package/config/skills/agent/core/calendar-create/SKILL.md +62 -0
  4. package/config/skills/agent/core/calendar-create/execute.sh +91 -0
  5. package/config/skills/agent/core/calendar-create/execute.test.sh +80 -0
  6. package/config/skills/agent/core/calendar-list/SKILL.md +58 -0
  7. package/config/skills/agent/core/calendar-list/execute.sh +88 -0
  8. package/config/skills/agent/core/calendar-list/execute.test.sh +71 -0
  9. package/config/skills/agent/core/gmail-read/SKILL.md +61 -0
  10. package/config/skills/agent/core/gmail-read/execute.sh +62 -0
  11. package/config/skills/agent/core/gmail-read/execute.test.sh +68 -0
  12. package/config/skills/agent/core/gmail-search/SKILL.md +64 -0
  13. package/config/skills/agent/core/gmail-search/execute.sh +70 -0
  14. package/config/skills/agent/core/gmail-search/execute.test.sh +67 -0
  15. package/config/skills/agent/core/gmail-send/SKILL.md +69 -0
  16. package/config/skills/agent/core/gmail-send/execute.sh +114 -0
  17. package/config/skills/agent/core/gmail-send/execute.test.sh +83 -0
  18. package/config/skills/orchestrator/list-missions/SKILL.md +47 -0
  19. package/config/skills/orchestrator/list-missions/execute.sh +34 -0
  20. package/config/skills/orchestrator/list-missions/execute.test.sh +33 -0
  21. package/config/slack-master-app-manifest.json +64 -0
  22. package/dist/backend/backend/src/constants.d.ts +125 -0
  23. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  24. package/dist/backend/backend/src/constants.js +125 -0
  25. package/dist/backend/backend/src/constants.js.map +1 -1
  26. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  27. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +3 -1
  28. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  29. package/dist/backend/backend/src/controllers/google/google.controller.d.ts +100 -0
  30. package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -0
  31. package/dist/backend/backend/src/controllers/google/google.controller.js +293 -0
  32. package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -0
  33. package/dist/backend/backend/src/controllers/google/google.routes.d.ts +26 -0
  34. package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -0
  35. package/dist/backend/backend/src/controllers/google/google.routes.js +38 -0
  36. package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -0
  37. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  38. package/dist/backend/backend/src/controllers/slack/slack.controller.js +209 -2
  39. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  40. package/dist/backend/backend/src/index.d.ts.map +1 -1
  41. package/dist/backend/backend/src/index.js +58 -8
  42. package/dist/backend/backend/src/index.js.map +1 -1
  43. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  44. package/dist/backend/backend/src/routes/api.routes.js +4 -0
  45. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  46. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts +8 -0
  47. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/cloud/cloud-initializer.js +25 -0
  49. package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
  51. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  52. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +1 -0
  53. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  54. package/dist/backend/backend/src/services/core/crewly-home.utils.d.ts +49 -0
  55. package/dist/backend/backend/src/services/core/crewly-home.utils.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/core/crewly-home.utils.js +86 -0
  57. package/dist/backend/backend/src/services/core/crewly-home.utils.js.map +1 -1
  58. package/dist/backend/backend/src/services/google/calendar.service.d.ts +98 -0
  59. package/dist/backend/backend/src/services/google/calendar.service.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/google/calendar.service.js +125 -0
  61. package/dist/backend/backend/src/services/google/calendar.service.js.map +1 -0
  62. package/dist/backend/backend/src/services/google/gmail.service.d.ts +165 -0
  63. package/dist/backend/backend/src/services/google/gmail.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/google/gmail.service.js +296 -0
  65. package/dist/backend/backend/src/services/google/gmail.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/google/google-api.client.d.ts +44 -0
  67. package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -0
  68. package/dist/backend/backend/src/services/google/google-api.client.js +104 -0
  69. package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -0
  70. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts +159 -0
  71. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/google/google-workspace-token.service.js +274 -0
  73. package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -0
  74. package/dist/backend/backend/src/services/memory/mission-context.service.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/memory/mission-context.service.js +2 -1
  76. package/dist/backend/backend/src/services/memory/mission-context.service.js.map +1 -1
  77. package/dist/backend/backend/src/services/memory/project-memory.service.d.ts.map +1 -1
  78. package/dist/backend/backend/src/services/memory/project-memory.service.js +3 -2
  79. package/dist/backend/backend/src/services/memory/project-memory.service.js.map +1 -1
  80. package/dist/backend/backend/src/services/memory/session-memory.service.d.ts.map +1 -1
  81. package/dist/backend/backend/src/services/memory/session-memory.service.js +2 -1
  82. package/dist/backend/backend/src/services/memory/session-memory.service.js.map +1 -1
  83. package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
  84. package/dist/backend/backend/src/services/project/task.service.js +2 -3
  85. package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
  86. package/dist/backend/backend/src/services/project/ticket-editor.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/project/ticket-editor.service.js +2 -3
  88. package/dist/backend/backend/src/services/project/ticket-editor.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.d.ts +12 -1
  90. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.js +30 -0
  92. package/dist/backend/backend/src/services/slack/slack-agent-identity.service.js.map +1 -1
  93. package/dist/backend/backend/src/services/slack/slack-cloud-config.service.d.ts +186 -0
  94. package/dist/backend/backend/src/services/slack/slack-cloud-config.service.d.ts.map +1 -0
  95. package/dist/backend/backend/src/services/slack/slack-cloud-config.service.js +409 -0
  96. package/dist/backend/backend/src/services/slack/slack-cloud-config.service.js.map +1 -0
  97. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts +95 -1
  98. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  99. package/dist/backend/backend/src/services/slack/slack-initializer.js +284 -4
  100. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  101. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.d.ts +178 -0
  102. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.d.ts.map +1 -0
  103. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.js +392 -0
  104. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.js.map +1 -0
  105. package/dist/backend/backend/src/services/slack/slack.service.d.ts +76 -1
  106. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  107. package/dist/backend/backend/src/services/slack/slack.service.js +234 -47
  108. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  109. package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  110. package/dist/backend/backend/src/services/v3/escalation-router.service.js +2 -1
  111. package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
  112. package/dist/backend/backend/src/services/v3/mission-paths.d.ts +17 -0
  113. package/dist/backend/backend/src/services/v3/mission-paths.d.ts.map +1 -1
  114. package/dist/backend/backend/src/services/v3/mission-paths.js +21 -2
  115. package/dist/backend/backend/src/services/v3/mission-paths.js.map +1 -1
  116. package/dist/backend/backend/src/services/v3/mission-reminder.service.d.ts +4 -0
  117. package/dist/backend/backend/src/services/v3/mission-reminder.service.d.ts.map +1 -1
  118. package/dist/backend/backend/src/services/v3/mission-reminder.service.js +17 -0
  119. package/dist/backend/backend/src/services/v3/mission-reminder.service.js.map +1 -1
  120. package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.d.ts +116 -0
  121. package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.d.ts.map +1 -0
  122. package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.js +254 -0
  123. package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.js.map +1 -0
  124. package/dist/backend/backend/src/services/v3/request.service.d.ts.map +1 -1
  125. package/dist/backend/backend/src/services/v3/request.service.js +2 -1
  126. package/dist/backend/backend/src/services/v3/request.service.js.map +1 -1
  127. package/dist/backend/backend/src/services/v3/task-projection.service.d.ts.map +1 -1
  128. package/dist/backend/backend/src/services/v3/task-projection.service.js +2 -1
  129. package/dist/backend/backend/src/services/v3/task-projection.service.js.map +1 -1
  130. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
  131. package/dist/backend/backend/src/services/v3/trigger-engine.service.js +2 -1
  132. package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
  133. package/dist/backend/backend/src/services/v3/v3-data.service.d.ts.map +1 -1
  134. package/dist/backend/backend/src/services/v3/v3-data.service.js +2 -1
  135. package/dist/backend/backend/src/services/v3/v3-data.service.js.map +1 -1
  136. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.d.ts.map +1 -1
  137. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js +6 -1
  138. package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js.map +1 -1
  139. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.d.ts.map +1 -1
  140. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js +3 -1
  141. package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js.map +1 -1
  142. package/dist/backend/backend/src/types/slack.types.d.ts +141 -2
  143. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  144. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  145. package/dist/backend/backend/src/types/v2/mission.types.d.ts +2 -0
  146. package/dist/backend/backend/src/types/v2/mission.types.d.ts.map +1 -1
  147. package/dist/backend/backend/src/types/v2/mission.types.js.map +1 -1
  148. package/dist/backend/build-info.json +2 -2
  149. package/dist/cli/backend/src/constants.d.ts +125 -0
  150. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  151. package/dist/cli/backend/src/constants.js +125 -0
  152. package/dist/cli/backend/src/constants.js.map +1 -1
  153. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
  154. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  155. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +1 -0
  156. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  157. package/dist/cli/backend/src/services/core/crewly-home.utils.d.ts +49 -0
  158. package/dist/cli/backend/src/services/core/crewly-home.utils.d.ts.map +1 -1
  159. package/dist/cli/backend/src/services/core/crewly-home.utils.js +86 -0
  160. package/dist/cli/backend/src/services/core/crewly-home.utils.js.map +1 -1
  161. package/dist/cli/backend/src/services/memory/mission-context.service.d.ts.map +1 -1
  162. package/dist/cli/backend/src/services/memory/mission-context.service.js +2 -1
  163. package/dist/cli/backend/src/services/memory/mission-context.service.js.map +1 -1
  164. package/dist/cli/backend/src/services/memory/project-memory.service.d.ts.map +1 -1
  165. package/dist/cli/backend/src/services/memory/project-memory.service.js +3 -2
  166. package/dist/cli/backend/src/services/memory/project-memory.service.js.map +1 -1
  167. package/dist/cli/backend/src/services/memory/session-memory.service.d.ts.map +1 -1
  168. package/dist/cli/backend/src/services/memory/session-memory.service.js +2 -1
  169. package/dist/cli/backend/src/services/memory/session-memory.service.js.map +1 -1
  170. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.d.ts +12 -1
  171. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.d.ts.map +1 -1
  172. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.js +30 -0
  173. package/dist/cli/backend/src/services/slack/slack-agent-identity.service.js.map +1 -1
  174. package/dist/cli/backend/src/services/slack/slack.service.d.ts +76 -1
  175. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  176. package/dist/cli/backend/src/services/slack/slack.service.js +234 -47
  177. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  178. package/dist/cli/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
  179. package/dist/cli/backend/src/services/v3/escalation-router.service.js +2 -1
  180. package/dist/cli/backend/src/services/v3/escalation-router.service.js.map +1 -1
  181. package/dist/cli/backend/src/services/v3/mission-paths.d.ts +17 -0
  182. package/dist/cli/backend/src/services/v3/mission-paths.d.ts.map +1 -1
  183. package/dist/cli/backend/src/services/v3/mission-paths.js +21 -2
  184. package/dist/cli/backend/src/services/v3/mission-paths.js.map +1 -1
  185. package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -1
  186. package/dist/cli/backend/src/services/v3/request.service.js +2 -1
  187. package/dist/cli/backend/src/services/v3/request.service.js.map +1 -1
  188. package/dist/cli/backend/src/services/v3/v3-data.service.d.ts.map +1 -1
  189. package/dist/cli/backend/src/services/v3/v3-data.service.js +2 -1
  190. package/dist/cli/backend/src/services/v3/v3-data.service.js.map +1 -1
  191. package/dist/cli/backend/src/types/slack.types.d.ts +141 -2
  192. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  193. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  194. package/dist/cli/backend/src/types/v2/mission.types.d.ts +2 -0
  195. package/dist/cli/backend/src/types/v2/mission.types.d.ts.map +1 -1
  196. package/dist/cli/backend/src/types/v2/mission.types.js.map +1 -1
  197. package/frontend/dist/assets/{index-019817e2.js → index-c87332bf.js} +273 -273
  198. package/frontend/dist/assets/{index-a56613e7.css → index-f6b4230e.css} +1 -1
  199. package/frontend/dist/index.html +2 -2
  200. package/package.json +1 -1
package/README.md CHANGED
@@ -125,9 +125,11 @@ Optional environment variables (`.env` file or shell):
125
125
  ```bash
126
126
  GEMINI_API_KEY=your_key_here # Required for Gemini CLI runtime
127
127
 
128
- SLACK_BOT_TOKEN=xoxb-... # Optional: Slack integration
128
+ SLACK_BOT_TOKEN=xoxb-... # Optional: self-hosted Slack app (see "Slack" below)
129
129
  SLACK_APP_TOKEN=xapp-...
130
130
  SLACK_SIGNING_SECRET=...
131
+ CREWLY_SLACK_SOURCE=cloud # Optional: env | cloud (unset = Cloud wins when both exist)
132
+ CREWLY_SLACK_PRIMARY=1 # Optional: this instance handles DMs / unmapped channels
131
133
 
132
134
  LOG_LEVEL=info # debug, info, warn, error
133
135
  WEB_PORT=8787 # Dashboard port (default: 8787)
@@ -135,6 +137,36 @@ CREWLY_BIND_HOST=0.0.0.0 # Interface to listen on (127.0.0.1 = this ma
135
137
  CREWLY_API_TOKEN=... # Pin the API token (otherwise generated at ~/.crewly/api-token)
136
138
  ```
137
139
 
140
+ ### Slack
141
+
142
+ **One click (recommended).** Log in to Crewly Cloud (Settings → Cloud), open
143
+ Settings → Integrations → Slack and press **Connect Slack**. Slack asks you to
144
+ approve the Crewly app once; from then on every Crewly instance signed in to the
145
+ same Crewly account gets Slack automatically — team channels, and one real Slack
146
+ bot user per agent — with no tokens to copy. The instance pulls its config from
147
+ Cloud on boot and every 10 minutes (cached 0600 at
148
+ `~/.crewly/slack-cloud-config.json`); Slack events reach it through the Cloud
149
+ relay, so no Socket Mode connection is opened.
150
+
151
+ **The one manual step.** Slack only lets an app be created by an *App
152
+ Configuration Token* of a workspace member, and Crewly creates one Slack app per
153
+ agent so each agent is a real bot user (name in the member list, native `@`
154
+ mentions). Generate the token at api.slack.com/apps ("Your App Configuration
155
+ Tokens" → Generate) and paste it in Settings → Slack → Agent Identities. Cloud
156
+ keeps it refreshed. Each new agent then shows an install link you click once.
157
+
158
+ **Several instances, one workspace.** Every instance registers its teams,
159
+ channels and agents with Cloud (on boot, on team changes, every 5 min); Slack
160
+ traffic for a team channel goes to the instance running that team. Direct
161
+ messages to the Crewly bot and channels no team owns go to the **primary**
162
+ instance — the Settings toggle, or `CREWLY_SLACK_PRIMARY=1`.
163
+
164
+ **Self-hosted app.** The original path (your own Slack app, Socket Mode,
165
+ `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` / `SLACK_SIGNING_SECRET` or the form under
166
+ Settings → Slack → Advanced) still works. `CREWLY_SLACK_SOURCE` picks the source:
167
+ `cloud` uses only the Cloud workspace, `env` uses only local tokens and never
168
+ asks Cloud, unset prefers Cloud when both exist (logged once at boot).
169
+
138
170
  ### Securing a server install
139
171
 
140
172
  Crewly agents run as real shells on the host, and `POST /api/terminal/:session/write`
@@ -194,17 +194,30 @@ bash {{ORCHESTRATOR_SKILLS_PATH}}/register-self/execute.sh '{"role":"orchestrato
194
194
 
195
195
  After registering, proceed to Step 4.
196
196
 
197
- ### Step 4 — Check Active Goals and Report
197
+ ### Step 4 — Check the Missions (OKRs) and Report
198
198
 
199
- After registration, check for active goals and OKRs:
199
+ After registration, read the real goal layer — the Missions API, not your
200
+ memory (memory only holds what someone happened to save):
200
201
 
201
202
  ```bash
202
- bash {{AGENT_SKILLS_PATH}}/core/recall/execute.sh '{"context":"OKR goals active tasks","scope":"both","agentId":"{{SESSION_ID}}","projectPath":"{{PROJECT_PATH}}"}'
203
+ bash {{ORCHESTRATOR_SKILLS_PATH}}/list-missions/execute.sh
203
204
  ```
204
205
 
205
- **If active OKRs or goals exist:** Report the current status to the user and ask if they want you to take over execution. Do NOT auto-execute unless the user explicitly activates Autonomous Mode (see below). Once the user activates Autonomous Mode in a session, it stays ON for the rest of that session — you do not need to re-ask.
206
+ **If a proposal is `pending_approval`:** tell the owner in one line what is
207
+ waiting (objective + its Key Results) and where to approve it (Missions page).
208
+ Do NOT execute, remind about or decompose it until it is approved.
206
209
 
207
- **If no active goals exist:** Say "Ready" and wait for the user.
210
+ **If live (approved) missions exist:** report each one's KRs (current → target,
211
+ status) in two or three lines and ask whether to take over execution. Do NOT
212
+ auto-execute unless the owner activates Autonomous Mode (below). Once
213
+ activated in a session it stays ON for that session.
214
+
215
+ **If there are no missions at all** and `get-team-status` shows teams that
216
+ completed work in the last week: draft ONE company-level OKR from that work
217
+ (objective + 2–3 Key Results that can be measured from something already in
218
+ the system — a ticket board, a build log, a folder count; see `count-json`
219
+ and `measurementSource: skill_output`) and ask the owner whether to create it.
220
+ Do not create it yourself. If there is no recent work either, say "Ready".
208
221
 
209
222
  ## Pipeline-First Planning Discipline (MANDATORY for planning intent)
210
223
 
@@ -271,6 +284,8 @@ The `delegate-task` skill emits a stderr WARNING when a brief is missing G/O/E m
271
284
 
272
285
  5. **Self-check before dispatch** — diff your composed `Expected Outcome` line items against the user's literal words. Every line in Expected Outcome must trace to a word/phrase the user actually wrote. If a line is justified only by recalled history, move it to `Context` or `Suggested Follow-ups` and surface back for confirmation.
273
286
 
287
+ 6. **Outbound mail under the owner's name is gated.** Sending email from the owner's Gmail (`gmail-send`, or any delegated brief that includes sending) requires the owner's explicit go-ahead in this conversation unless the task brief already grants it in `Decision Rights`; without that grant, draft with the dry-run preview and show it for approval instead of sending.
288
+
274
289
  **Negative pattern to suppress** (the 2026-05-20 ESTestNode shape):
275
290
  > User: "update X" → ORC: `recall_memory(X)` → memory has "X had 3 known issues" → ORC: "while you're updating X, handle the 3 known issues too" → executor does all 4 things → user pays for unauthorized scope on prod.
276
291
 
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: Calendar Create
3
+ description: Create an event on the owner's Google Calendar (via the Google Workspace grant held by Crewly Cloud) — timed or all-day, with optional description and attendees. Attendees receive an invitation from the owner, so confirm before inviting others.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - operations
11
+ - ops
12
+ - sales
13
+ - support
14
+ - generalist
15
+ triggers:
16
+ - create event
17
+ - schedule meeting
18
+ - add to calendar
19
+ - book a meeting
20
+ tags:
21
+ - google
22
+ - calendar
23
+ execution:
24
+ type: script
25
+ script:
26
+ file: execute.sh
27
+ interpreter: bash
28
+ timeoutMs: 30000
29
+ ---
30
+
31
+ # Calendar Create
32
+
33
+ ```bash
34
+ bash execute.sh --summary "Design review" --start 2026-09-20T14:00:00 --end 2026-09-20T15:00:00 \
35
+ --timezone Asia/Shanghai --description "Walk through v2" --attendee ann@example.com
36
+ bash execute.sh --summary "Offsite" --start 2026-10-01 --end 2026-10-02 # all-day (end is exclusive)
37
+ bash execute.sh '{"summary":"Standup","start":"2026-09-21T09:00:00Z","end":"2026-09-21T09:15:00Z"}'
38
+ ```
39
+
40
+ ## Parameters
41
+
42
+ | Name | Required | Meaning |
43
+ |---|---|---|
44
+ | `--summary` (`summary`) | yes | Event title |
45
+ | `--start` (`start`) | yes | ISO 8601 date-time, or `YYYY-MM-DD` for all-day |
46
+ | `--end` (`end`) | yes | ISO 8601 date-time, or `YYYY-MM-DD` (exclusive) for all-day |
47
+ | `--timezone` (`timezone`) | no | IANA zone applied to start/end without an offset |
48
+ | `--description` (`description`) | no | Body text |
49
+ | `--attendee` (`attendees`) | no | Email; repeat for several (JSON: array or comma list) |
50
+ | `--calendar` (`calendarId`) | no | Calendar id; default `primary` |
51
+
52
+ ## Output
53
+
54
+ ```json
55
+ {"success":true,"id":"…","summary":"Design review","start":{"dateTime":"…","timeZone":"Asia/Shanghai"},"end":{…},"htmlLink":"https://…","attendees":[{"email":"ann@example.com","responseStatus":"needsAction"}]}
56
+ ```
57
+
58
+ ## Failures
59
+
60
+ Missing `--summary` / `--start` / `--end` → `{"error":"Missing required parameter: …"}` on stderr, exit 1.
61
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1) when the owner has not connected Google Workspace;
62
+ `reason` `validation` when Google cannot parse a time.
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # calendar-create — Create an event on the owner's Google Calendar
4
+ #
5
+ # Backed by POST /api/google/calendar/events.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --summary "Review" --start 2026-09-20T14:00:00 --end 2026-09-20T15:00:00
9
+ # [--timezone Asia/Shanghai] [--description "…"] [--attendee a@b.c]... [--calendar id]
10
+ # bash execute.sh '{"summary":"Review","start":"…","end":"…","attendees":["a@b.c"]}'
11
+ # =============================================================================
12
+ set -euo pipefail
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
15
+
16
+ print_usage() {
17
+ cat <<'EOF_USAGE'
18
+ Usage:
19
+ bash execute.sh --summary "Review" --start <ISO|YYYY-MM-DD> --end <ISO|YYYY-MM-DD>
20
+ [--timezone Asia/Shanghai] [--description "…"] [--attendee a@b.c]... [--calendar <id>]
21
+ bash execute.sh '{"summary":"Review","start":"…","end":"…","attendees":["a@b.c"]}'
22
+
23
+ Options:
24
+ --summary Event title (required)
25
+ --start Start: ISO 8601 date-time, or YYYY-MM-DD for all-day (required)
26
+ --end End: ISO 8601 date-time, or YYYY-MM-DD (exclusive) for all-day (required)
27
+ --timezone IANA zone for start/end without an offset
28
+ --description Body text
29
+ --attendee Attendee email (repeat for several)
30
+ --calendar Calendar id (default: primary)
31
+ --help | -h Show this help
32
+ EOF_USAGE
33
+ }
34
+
35
+ INPUT_JSON=""
36
+ SUMMARY=""; START=""; END=""; TIMEZONE=""; DESCRIPTION=""; CALENDAR=""
37
+ ATTENDEES_JSON='[]'
38
+
39
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
40
+ INPUT_JSON="$1"
41
+ shift || true
42
+ fi
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --summary|--title) [ $# -ge 2 ] || error_exit "--summary requires a value"; SUMMARY="$2"; shift 2 ;;
47
+ --start) [ $# -ge 2 ] || error_exit "--start requires a value"; START="$2"; shift 2 ;;
48
+ --end) [ $# -ge 2 ] || error_exit "--end requires a value"; END="$2"; shift 2 ;;
49
+ --timezone|--tz) [ $# -ge 2 ] || error_exit "--timezone requires a value"; TIMEZONE="$2"; shift 2 ;;
50
+ --description) [ $# -ge 2 ] || error_exit "--description requires a value"; DESCRIPTION="$2"; shift 2 ;;
51
+ --calendar|-c) [ $# -ge 2 ] || error_exit "--calendar requires a value"; CALENDAR="$2"; shift 2 ;;
52
+ --attendee) [ $# -ge 2 ] || error_exit "--attendee requires a value"
53
+ ATTENDEES_JSON=$(jq -cn --argjson arr "$ATTENDEES_JSON" --arg v "$2" '$arr + [$v]'); shift 2 ;;
54
+ --help|-h) print_usage; exit 0 ;;
55
+ *) error_exit "Unknown option: $1" ;;
56
+ esac
57
+ done
58
+
59
+ if [ -n "$INPUT_JSON" ]; then
60
+ INPUT=$(read_json_input "$INPUT_JSON")
61
+ [ -z "$SUMMARY" ] && SUMMARY=$(printf '%s' "$INPUT" | jq -r '.summary // .title // empty')
62
+ [ -z "$START" ] && START=$(printf '%s' "$INPUT" | jq -r '.start // empty')
63
+ [ -z "$END" ] && END=$(printf '%s' "$INPUT" | jq -r '.end // empty')
64
+ [ -z "$TIMEZONE" ] && TIMEZONE=$(printf '%s' "$INPUT" | jq -r '.timezone // .timeZone // empty')
65
+ [ -z "$DESCRIPTION" ] && DESCRIPTION=$(printf '%s' "$INPUT" | jq -r '.description // empty')
66
+ [ -z "$CALENDAR" ] && CALENDAR=$(printf '%s' "$INPUT" | jq -r '.calendarId // .calendar // empty')
67
+ if [ "$(printf '%s' "$ATTENDEES_JSON" | jq 'length')" -eq 0 ]; then
68
+ ATTENDEES_JSON=$(printf '%s' "$INPUT" | jq -c '(.attendees // []) | if type == "string" then split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(. != "")) else . end')
69
+ fi
70
+ fi
71
+
72
+ require_param "summary (--summary)" "$SUMMARY"
73
+ require_param "start (--start)" "$START"
74
+ require_param "end (--end)" "$END"
75
+
76
+ BODY=$(jq -cn --arg summary "$SUMMARY" --arg start "$START" --arg end "$END" \
77
+ --arg timezone "$TIMEZONE" --arg description "$DESCRIPTION" --arg calendarId "$CALENDAR" \
78
+ --argjson attendees "$ATTENDEES_JSON" \
79
+ '{summary: $summary, start: $start, end: $end}
80
+ + (if $timezone != "" then {timezone: $timezone} else {} end)
81
+ + (if $description != "" then {description: $description} else {} end)
82
+ + (if $calendarId != "" then {calendarId: $calendarId} else {} end)
83
+ + (if ($attendees | length) > 0 then {attendees: $attendees} else {} end)')
84
+
85
+ RESPONSE=$(api_call POST "/google/calendar/events" "$BODY" 2>&1) || {
86
+ printf '%s' "$RESPONSE" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
87
+ || jq -n --arg r "$RESPONSE" '{success: false, reason: $r}'
88
+ exit 1
89
+ }
90
+
91
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false)} + (.data | {id, summary, start, end, htmlLink, attendees})'
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # Tests for calendar-create — run with: bash execute.test.sh
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=18805
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 do_POST(self):
22
+ n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
23
+ data = json.loads(body or '{}')
24
+ open(LOG, 'w').write(json.dumps({'method': 'POST', 'path': self.path, 'body': data}))
25
+ if data.get('summary') == 'no-grant':
26
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
27
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
28
+ if data.get('start') == 'tomorrow':
29
+ self.send_response(400); self.send_header('Content-Type', 'application/json'); self.end_headers()
30
+ self.wfile.write(json.dumps({'success': False, 'error': 'validation', 'message': '"start" must be YYYY-MM-DD or an ISO 8601 date-time', 'hint': 'Fix the request and retry.'}).encode()); return
31
+ tz = data.get('timezone')
32
+ def t(v):
33
+ if len(v) == 10: return {'date': v}
34
+ return {'dateTime': v, 'timeZone': tz} if tz else {'dateTime': v}
35
+ ev = {'id': 'new1', 'summary': data['summary'], 'start': t(data['start']), 'end': t(data['end']), 'htmlLink': 'https://cal/new1',
36
+ 'attendees': [{'email': a, 'responseStatus': 'needsAction'} for a in data.get('attendees', [])], 'status': 'confirmed'}
37
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
38
+ self.wfile.write(json.dumps({'success': True, 'data': ev}).encode())
39
+ def log_message(self, *a, **k): pass
40
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
41
+ PY
42
+ STUB_PID=$!
43
+ disown "$STUB_PID" 2>/dev/null || true
44
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
45
+ 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
46
+
47
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
48
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
49
+
50
+ # --- timed event with everything ---
51
+ OUT=$(run --summary "Design review" --start 2026-09-20T14:00:00 --end 2026-09-20T15:00:00 --timezone Asia/Shanghai \
52
+ --description "Walk through v2" --attendee ann@example.com --attendee bob@example.com --calendar ops@example.com)
53
+ check "flags: body" "$(jq -c .body "$STUB_LOG")" '{"summary":"Design review","start":"2026-09-20T14:00:00","end":"2026-09-20T15:00:00","timezone":"Asia/Shanghai","description":"Walk through v2","calendarId":"ops@example.com","attendees":["ann@example.com","bob@example.com"]}'
54
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/google/calendar/events"
55
+ check "flags: output" "$OUT" '{"success":true,"id":"new1","summary":"Design review","start":{"dateTime":"2026-09-20T14:00:00","timeZone":"Asia/Shanghai"},"end":{"dateTime":"2026-09-20T15:00:00","timeZone":"Asia/Shanghai"},"htmlLink":"https://cal/new1","attendees":[{"email":"ann@example.com","responseStatus":"needsAction"},{"email":"bob@example.com","responseStatus":"needsAction"}]}'
56
+
57
+ # --- all-day via JSON, comma-separated attendees ---
58
+ OUT=$(run '{"summary":"Offsite","start":"2026-10-01","end":"2026-10-02","attendees":"a@b.c, d@e.f"}')
59
+ check "json: body omits optional keys, splits attendees" "$(jq -c .body "$STUB_LOG")" '{"summary":"Offsite","start":"2026-10-01","end":"2026-10-02","attendees":["a@b.c","d@e.f"]}'
60
+ check "json: all-day output" "$(printf '%s' "$OUT" | jq -c '{start, end}')" '{"start":{"date":"2026-10-01"},"end":{"date":"2026-10-02"}}'
61
+
62
+ # --- backend validation surfaces ---
63
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --summary x --start tomorrow --end 2026-10-02 2>/dev/null); RC=$?
64
+ check "400: exit code" "$RC" "1"
65
+ check "400: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "validation"
66
+
67
+ # --- 409 ---
68
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --summary no-grant --start 2026-10-01 --end 2026-10-02 2>/dev/null)
69
+ check "409: reason + hint" "$OUT" '{"success":false,"reason":"not_connected","hint":"https://cloud/start?token=j","message":"no grant"}'
70
+
71
+ # --- local validation (nothing sent) ---
72
+ : > "$STUB_LOG"
73
+ check "missing summary errors" "$(run_err --start 2026-10-01 --end 2026-10-02)" "Missing required parameter: summary (--summary)"
74
+ check "missing start errors" "$(run_err --summary x --end 2026-10-02)" "Missing required parameter: start (--start)"
75
+ check "missing end errors" "$(run_err --summary x --start 2026-10-01)" "Missing required parameter: end (--end)"
76
+ check "validation: no request made" "$(cat "$STUB_LOG")" ""
77
+ check "unknown option errors" "$(run_err --bogus 1)" "Unknown option: --bogus"
78
+
79
+ echo "=== Results: $PASS passed, $FAIL failed ==="
80
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: Calendar List
3
+ description: List upcoming events on the owner's Google Calendar (via the Google Workspace grant held by Crewly Cloud). Defaults to the next 7 days on the primary calendar. Read-only.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - developer
11
+ - operations
12
+ - ops
13
+ - sales
14
+ - support
15
+ - generalist
16
+ triggers:
17
+ - list calendar
18
+ - check calendar
19
+ - what's on my calendar
20
+ - upcoming meetings
21
+ tags:
22
+ - google
23
+ - calendar
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 30000
30
+ ---
31
+
32
+ # Calendar List
33
+
34
+ ```bash
35
+ bash execute.sh # next 7 days, primary calendar
36
+ bash execute.sh --from 2026-09-18T00:00:00Z --to 2026-09-19T00:00:00Z --max 20
37
+ bash execute.sh '{"calendarId":"team@group.calendar.google.com","from":"…","to":"…"}'
38
+ ```
39
+
40
+ ## Parameters
41
+
42
+ | Name | Required | Meaning |
43
+ |---|---|---|
44
+ | `--from` (`from`) | no | ISO 8601 lower bound; default now |
45
+ | `--to` (`to`) | no | ISO 8601 upper bound; default now + 7 days |
46
+ | `--calendar` (`calendarId`) | no | Calendar id; default `primary` |
47
+ | `--max` (`max`) | no | Result cap, default 50, max 250 |
48
+
49
+ ## Output
50
+
51
+ ```json
52
+ {"count":2,"events":[{"id":"…","summary":"Standup","start":{"dateTime":"2026-09-18T09:00:00+08:00"},"end":{"dateTime":"…"},"attendees":[{"email":"ann@example.com","responseStatus":"accepted"}],"htmlLink":"https://…"}]}
53
+ ```
54
+
55
+ ## Failures
56
+
57
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
58
+ when the owner has not connected Google Workspace.
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # calendar-list — Upcoming events on the owner's Google Calendar (read-only)
4
+ #
5
+ # Backed by GET /api/google/calendar/events?from=&to=&calendarId=&max=.
6
+ # Without --from/--to the window is now → now + 7 days.
7
+ #
8
+ # Usage:
9
+ # bash execute.sh [--from ISO] [--to ISO] [--calendar id] [--max 20]
10
+ # bash execute.sh '{"from":"…","to":"…","calendarId":"primary","max":20}'
11
+ # =============================================================================
12
+ set -euo pipefail
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
15
+
16
+ print_usage() {
17
+ cat <<'EOF_USAGE'
18
+ Usage:
19
+ bash execute.sh [--from ISO-8601] [--to ISO-8601] [--calendar <id>] [--max 20]
20
+ bash execute.sh '{"from":"…","to":"…","calendarId":"primary","max":20}'
21
+
22
+ Options:
23
+ --from Lower bound (default: now)
24
+ --to Upper bound (default: now + 7 days)
25
+ --calendar Calendar id (default: primary)
26
+ --max Result cap (default 50, max 250)
27
+ --help | -h Show this help
28
+ EOF_USAGE
29
+ }
30
+
31
+ DEFAULT_WINDOW_DAYS="${CREWLY_CALENDAR_LIST_DAYS:-7}"
32
+
33
+ # Portable "now + N days" in UTC ISO 8601 (macOS `date -v`, GNU `date -d`).
34
+ iso_now_plus_days() {
35
+ local days="$1"
36
+ if date -u -v+"${days}"d +%Y-%m-%dT%H:%M:%SZ >/dev/null 2>&1; then
37
+ date -u -v+"${days}"d +%Y-%m-%dT%H:%M:%SZ
38
+ else
39
+ date -u -d "+${days} days" +%Y-%m-%dT%H:%M:%SZ
40
+ fi
41
+ }
42
+
43
+ INPUT_JSON=""
44
+ FROM=""; TO=""; CALENDAR=""; MAX=""
45
+
46
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
47
+ INPUT_JSON="$1"
48
+ shift || true
49
+ fi
50
+
51
+ while [[ $# -gt 0 ]]; do
52
+ case "$1" in
53
+ --from) [ $# -ge 2 ] || error_exit "--from requires a value"; FROM="$2"; shift 2 ;;
54
+ --to) [ $# -ge 2 ] || error_exit "--to requires a value"; TO="$2"; shift 2 ;;
55
+ --calendar|-c) [ $# -ge 2 ] || error_exit "--calendar requires a value"; CALENDAR="$2"; shift 2 ;;
56
+ --max|-n) [ $# -ge 2 ] || error_exit "--max requires a value"; MAX="$2"; shift 2 ;;
57
+ --help|-h) print_usage; exit 0 ;;
58
+ *) error_exit "Unknown option: $1" ;;
59
+ esac
60
+ done
61
+
62
+ if [ -n "$INPUT_JSON" ]; then
63
+ INPUT=$(read_json_input "$INPUT_JSON")
64
+ [ -z "$FROM" ] && FROM=$(printf '%s' "$INPUT" | jq -r '.from // .timeMin // empty')
65
+ [ -z "$TO" ] && TO=$(printf '%s' "$INPUT" | jq -r '.to // .timeMax // empty')
66
+ [ -z "$CALENDAR" ] && CALENDAR=$(printf '%s' "$INPUT" | jq -r '.calendarId // .calendar // empty')
67
+ [ -z "$MAX" ] && MAX=$(printf '%s' "$INPUT" | jq -r '.max // empty')
68
+ fi
69
+
70
+ [ -z "$FROM" ] && FROM=$(date -u +%Y-%m-%dT%H:%M:%SZ)
71
+ [ -z "$TO" ] && TO=$(iso_now_plus_days "$DEFAULT_WINDOW_DAYS")
72
+
73
+ QS="from=$(jq -rn --arg v "$FROM" '$v|@uri')&to=$(jq -rn --arg v "$TO" '$v|@uri')"
74
+ [ -n "$CALENDAR" ] && QS="${QS}&calendarId=$(jq -rn --arg v "$CALENDAR" '$v|@uri')"
75
+ [ -n "$MAX" ] && QS="${QS}&max=$(jq -rn --arg v "$MAX" '$v|@uri')"
76
+
77
+ RESPONSE=$(api_call GET "/google/calendar/events?${QS}" 2>&1) || {
78
+ printf '%s' "$RESPONSE" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
79
+ || jq -n --arg r "$RESPONSE" '{success: false, reason: $r}'
80
+ exit 1
81
+ }
82
+
83
+ # Oversized bodies are parked on disk by api_call; pass that envelope through.
84
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
85
+ printf '%s\n' "$RESPONSE"; exit 0
86
+ fi
87
+
88
+ printf '%s' "$RESPONSE" | jq -c '{count: .data.count, events: .data.events}'
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ # Tests for calendar-list — run with: bash execute.test.sh
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=18804
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
+ EV = {'id': 'e1', 'summary': 'Standup', 'start': {'dateTime': '2026-09-18T09:00:00Z'}, 'end': {'dateTime': '2026-09-18T09:15:00Z'}, 'attendees': []}
22
+ class H(BaseHTTPRequestHandler):
23
+ def do_GET(self):
24
+ open(LOG, 'w').write(json.dumps({'method': 'GET', 'path': self.path}))
25
+ qs = parse_qs(urlparse(self.path).query)
26
+ if qs.get('calendarId', [''])[0] == 'no-grant':
27
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
28
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
29
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
30
+ self.wfile.write(json.dumps({'success': True, 'data': {'count': 1, 'events': [EV]}}).encode())
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
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
37
+ 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
38
+
39
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
40
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
41
+
42
+ WANT='{"count":1,"events":[{"id":"e1","summary":"Standup","start":{"dateTime":"2026-09-18T09:00:00Z"},"end":{"dateTime":"2026-09-18T09:15:00Z"},"attendees":[]}]}'
43
+
44
+ OUT=$(run --from 2026-09-18T00:00:00Z --to 2026-09-19T00:00:00Z --calendar "team@group.calendar.google.com" --max 20)
45
+ check "flags: output" "$OUT" "$WANT"
46
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/google/calendar/events?from=2026-09-18T00%3A00%3A00Z&to=2026-09-19T00%3A00%3A00Z&calendarId=team%40group.calendar.google.com&max=20"
47
+
48
+ OUT=$(run '{"from":"2026-09-18T00:00:00Z","to":"2026-09-19T00:00:00Z"}')
49
+ check "json: output" "$OUT" "$WANT"
50
+ check "json: path without optional params" "$(jq -r .path "$STUB_LOG")" "/api/google/calendar/events?from=2026-09-18T00%3A00%3A00Z&to=2026-09-19T00%3A00%3A00Z"
51
+
52
+ # --- defaults: now → now + 7 days (shape only; values are wall-clock) ---
53
+ run >/dev/null
54
+ P=$(jq -r .path "$STUB_LOG")
55
+ FROM_V=$(printf '%s' "$P" | sed -E 's/.*[?&]from=([^&]*).*/\1/')
56
+ TO_V=$(printf '%s' "$P" | sed -E 's/.*[?&]to=([^&]*).*/\1/')
57
+ [[ "$FROM_V" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}%3A[0-9]{2}%3A[0-9]{2}Z$ ]] && FROM_OK=yes || FROM_OK=no
58
+ [[ "$TO_V" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}%3A[0-9]{2}%3A[0-9]{2}Z$ ]] && TO_OK=yes || TO_OK=no
59
+ check "defaults: from is ISO UTC" "$FROM_OK" "yes"
60
+ check "defaults: to is ISO UTC" "$TO_OK" "yes"
61
+ [ "$TO_V" \> "$FROM_V" ] && ORDER=yes || ORDER=no
62
+ check "defaults: to is after from" "$ORDER" "yes"
63
+
64
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --calendar no-grant 2>/dev/null); RC=$?
65
+ check "409: exit code" "$RC" "1"
66
+ check "409: reason + hint" "$OUT" '{"success":false,"reason":"not_connected","hint":"https://cloud/start?token=j","message":"no grant"}'
67
+
68
+ check "unknown option errors" "$(run_err --bogus 1)" "Unknown option: --bogus"
69
+
70
+ echo "=== Results: $PASS passed, $FAIL failed ==="
71
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: Gmail Read
3
+ description: Read one message from the owner's Gmail by id — headers, decoded body (plain text, HTML stripped as fallback) and a list of attachments (names only, nothing downloaded). Read-only.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - developer
11
+ - operations
12
+ - ops
13
+ - sales
14
+ - support
15
+ - generalist
16
+ triggers:
17
+ - read email
18
+ - open email
19
+ - show email
20
+ tags:
21
+ - google
22
+ - gmail
23
+ - email
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 30000
30
+ ---
31
+
32
+ # Gmail Read
33
+
34
+ Fetches one message by the `id` a `gmail-search` hit returned.
35
+
36
+ ```bash
37
+ bash execute.sh --id 18f0a1b2c3d4e5f6
38
+ bash execute.sh '{"id":"18f0a1b2c3d4e5f6"}'
39
+ ```
40
+
41
+ ## Parameters
42
+
43
+ | Name | Required | Meaning |
44
+ |---|---|---|
45
+ | `--id` / `-i` (`id`) | yes | Gmail message id |
46
+
47
+ ## Output
48
+
49
+ ```json
50
+ {"id":"18f…","threadId":"18f…","from":"Ann <ann@example.com>","to":"owner@example.com","cc":"","subject":"Q3 numbers","date":"…","messageId":"<abc@mail.example.com>","body":"Hi —\n…","bodyType":"text","attachments":[{"filename":"deck.pdf","mimeType":"application/pdf","size":12345,"attachmentId":"ANGjd…"}]}
51
+ ```
52
+
53
+ `bodyType` is `text` (text/plain part), `html` (stripped from text/html)
54
+ or `none`. Attachments are listed, never downloaded. To reply in-thread,
55
+ pass `threadId` and `messageId` to `gmail-send`.
56
+
57
+ ## Failures
58
+
59
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
60
+ when the owner has not connected Google Workspace; `reason` `google_error`
61
+ with a 404 message when the id is unknown.