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.
- package/README.md +33 -1
- package/config/roles/orchestrator/prompt.md +20 -5
- package/config/skills/agent/core/calendar-create/SKILL.md +62 -0
- package/config/skills/agent/core/calendar-create/execute.sh +91 -0
- package/config/skills/agent/core/calendar-create/execute.test.sh +80 -0
- package/config/skills/agent/core/calendar-list/SKILL.md +58 -0
- package/config/skills/agent/core/calendar-list/execute.sh +88 -0
- package/config/skills/agent/core/calendar-list/execute.test.sh +71 -0
- package/config/skills/agent/core/gmail-read/SKILL.md +61 -0
- package/config/skills/agent/core/gmail-read/execute.sh +62 -0
- package/config/skills/agent/core/gmail-read/execute.test.sh +68 -0
- package/config/skills/agent/core/gmail-search/SKILL.md +64 -0
- package/config/skills/agent/core/gmail-search/execute.sh +70 -0
- package/config/skills/agent/core/gmail-search/execute.test.sh +67 -0
- package/config/skills/agent/core/gmail-send/SKILL.md +69 -0
- package/config/skills/agent/core/gmail-send/execute.sh +114 -0
- package/config/skills/agent/core/gmail-send/execute.test.sh +83 -0
- package/config/skills/orchestrator/list-missions/SKILL.md +47 -0
- package/config/skills/orchestrator/list-missions/execute.sh +34 -0
- package/config/skills/orchestrator/list-missions/execute.test.sh +33 -0
- package/config/slack-master-app-manifest.json +64 -0
- package/dist/backend/backend/src/constants.d.ts +125 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +125 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts +100 -0
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/google/google.controller.js +293 -0
- package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts +26 -0
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/google/google.routes.js +38 -0
- package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +209 -2
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +58 -8
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +4 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts +8 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js +25 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/backend/backend/src/services/core/crewly-home.utils.d.ts +49 -0
- package/dist/backend/backend/src/services/core/crewly-home.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/services/core/crewly-home.utils.js +86 -0
- package/dist/backend/backend/src/services/core/crewly-home.utils.js.map +1 -1
- package/dist/backend/backend/src/services/google/calendar.service.d.ts +98 -0
- package/dist/backend/backend/src/services/google/calendar.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/calendar.service.js +125 -0
- package/dist/backend/backend/src/services/google/calendar.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/gmail.service.d.ts +165 -0
- package/dist/backend/backend/src/services/google/gmail.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/gmail.service.js +296 -0
- package/dist/backend/backend/src/services/google/gmail.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/google-api.client.d.ts +44 -0
- package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/google-api.client.js +104 -0
- package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts +159 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.js +274 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -0
- package/dist/backend/backend/src/services/memory/mission-context.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/mission-context.service.js +2 -1
- package/dist/backend/backend/src/services/memory/mission-context.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/project-memory.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/project-memory.service.js +3 -2
- package/dist/backend/backend/src/services/memory/project-memory.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/session-memory.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/session-memory.service.js +2 -1
- package/dist/backend/backend/src/services/memory/session-memory.service.js.map +1 -1
- package/dist/backend/backend/src/services/project/task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project/task.service.js +2 -3
- package/dist/backend/backend/src/services/project/task.service.js.map +1 -1
- package/dist/backend/backend/src/services/project/ticket-editor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project/ticket-editor.service.js +2 -3
- package/dist/backend/backend/src/services/project/ticket-editor.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-identity.service.d.ts +12 -1
- package/dist/backend/backend/src/services/slack/slack-agent-identity.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-identity.service.js +30 -0
- package/dist/backend/backend/src/services/slack/slack-agent-identity.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-cloud-config.service.d.ts +186 -0
- package/dist/backend/backend/src/services/slack/slack-cloud-config.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-cloud-config.service.js +409 -0
- package/dist/backend/backend/src/services/slack/slack-cloud-config.service.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts +95 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-initializer.js +284 -4
- package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-instance-registry.service.d.ts +178 -0
- package/dist/backend/backend/src/services/slack/slack-instance-registry.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-instance-registry.service.js +392 -0
- package/dist/backend/backend/src/services/slack/slack-instance-registry.service.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack.service.d.ts +76 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +234 -47
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +2 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/mission-paths.d.ts +17 -0
- package/dist/backend/backend/src/services/v3/mission-paths.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/mission-paths.js +21 -2
- package/dist/backend/backend/src/services/v3/mission-paths.js.map +1 -1
- package/dist/backend/backend/src/services/v3/mission-reminder.service.d.ts +4 -0
- package/dist/backend/backend/src/services/v3/mission-reminder.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/mission-reminder.service.js +17 -0
- package/dist/backend/backend/src/services/v3/mission-reminder.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.d.ts +116 -0
- package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.js +254 -0
- package/dist/backend/backend/src/services/v3/okr-owner-guidance.service.js.map +1 -0
- package/dist/backend/backend/src/services/v3/request.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request.service.js +2 -1
- package/dist/backend/backend/src/services/v3/request.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/task-projection.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/task-projection.service.js +2 -1
- package/dist/backend/backend/src/services/v3/task-projection.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js +2 -1
- package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/v3-data.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/v3-data.service.js +2 -1
- package/dist/backend/backend/src/services/v3/v3-data.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js +6 -1
- package/dist/backend/backend/src/services/wiki/wiki-bookkeep-trigger.service.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js +3 -1
- package/dist/backend/backend/src/services/wiki/wiki-workitem-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/types/slack.types.d.ts +141 -2
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/mission.types.d.ts +2 -0
- package/dist/backend/backend/src/types/v2/mission.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/mission.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +125 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +125 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/cli/backend/src/services/core/crewly-home.utils.d.ts +49 -0
- package/dist/cli/backend/src/services/core/crewly-home.utils.d.ts.map +1 -1
- package/dist/cli/backend/src/services/core/crewly-home.utils.js +86 -0
- package/dist/cli/backend/src/services/core/crewly-home.utils.js.map +1 -1
- package/dist/cli/backend/src/services/memory/mission-context.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/mission-context.service.js +2 -1
- package/dist/cli/backend/src/services/memory/mission-context.service.js.map +1 -1
- package/dist/cli/backend/src/services/memory/project-memory.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/project-memory.service.js +3 -2
- package/dist/cli/backend/src/services/memory/project-memory.service.js.map +1 -1
- package/dist/cli/backend/src/services/memory/session-memory.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/session-memory.service.js +2 -1
- package/dist/cli/backend/src/services/memory/session-memory.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-identity.service.d.ts +12 -1
- package/dist/cli/backend/src/services/slack/slack-agent-identity.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-agent-identity.service.js +30 -0
- package/dist/cli/backend/src/services/slack/slack-agent-identity.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +76 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +234 -47
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/escalation-router.service.js +2 -1
- package/dist/cli/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/mission-paths.d.ts +17 -0
- package/dist/cli/backend/src/services/v3/mission-paths.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/mission-paths.js +21 -2
- package/dist/cli/backend/src/services/v3/mission-paths.js.map +1 -1
- package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/request.service.js +2 -1
- package/dist/cli/backend/src/services/v3/request.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/v3-data.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/v3-data.service.js +2 -1
- package/dist/cli/backend/src/services/v3/v3-data.service.js.map +1 -1
- package/dist/cli/backend/src/types/slack.types.d.ts +141 -2
- package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/slack.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/mission.types.d.ts +2 -0
- package/dist/cli/backend/src/types/v2/mission.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/mission.types.js.map +1 -1
- package/frontend/dist/assets/{index-019817e2.js → index-c87332bf.js} +273 -273
- package/frontend/dist/assets/{index-a56613e7.css → index-f6b4230e.css} +1 -1
- package/frontend/dist/index.html +2 -2
- 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
|
|
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
|
|
197
|
+
### Step 4 — Check the Missions (OKRs) and Report
|
|
198
198
|
|
|
199
|
-
After registration,
|
|
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 {{
|
|
203
|
+
bash {{ORCHESTRATOR_SKILLS_PATH}}/list-missions/execute.sh
|
|
203
204
|
```
|
|
204
205
|
|
|
205
|
-
**If
|
|
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
|
|
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.
|