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
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # gmail-read — Read one message from the owner's Gmail (read-only)
4
+ #
5
+ # Backed by GET /api/google/gmail/messages/:id.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --id 18f0a1b2c3d4e5f6
9
+ # bash execute.sh '{"id":"18f0a1b2c3d4e5f6"}'
10
+ # =============================================================================
11
+ set -euo pipefail
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
14
+
15
+ print_usage() {
16
+ cat <<'EOF_USAGE'
17
+ Usage:
18
+ bash execute.sh --id <gmail message id>
19
+ bash execute.sh '{"id":"<gmail message id>"}'
20
+
21
+ Options:
22
+ --id | -i Gmail message id (required; from gmail-search)
23
+ --help | -h Show this help
24
+ EOF_USAGE
25
+ }
26
+
27
+ INPUT_JSON=""
28
+ ID=""
29
+
30
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
31
+ INPUT_JSON="$1"
32
+ shift || true
33
+ fi
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --id|-i) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
38
+ --help|-h) print_usage; exit 0 ;;
39
+ *) error_exit "Unknown option: $1" ;;
40
+ esac
41
+ done
42
+
43
+ if [ -n "$INPUT_JSON" ]; then
44
+ INPUT=$(read_json_input "$INPUT_JSON")
45
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .messageId // empty')
46
+ fi
47
+
48
+ require_param "id (--id)" "$ID"
49
+
50
+ ID_ENC=$(jq -rn --arg v "$ID" '$v|@uri')
51
+ RESPONSE=$(api_call GET "/google/gmail/messages/${ID_ENC}" 2>&1) || {
52
+ printf '%s' "$RESPONSE" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
53
+ || jq -n --arg r "$RESPONSE" '{success: false, reason: $r}'
54
+ exit 1
55
+ }
56
+
57
+ # Oversized bodies are parked on disk by api_call; pass that envelope through.
58
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
59
+ printf '%s\n' "$RESPONSE"; exit 0
60
+ fi
61
+
62
+ printf '%s' "$RESPONSE" | jq -c '.data | {id, threadId, from, to, cc, subject, date, messageId, body, bodyType, attachments}'
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+ # Tests for gmail-read — 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=18802
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
+ MSG = {'id': 'm1', 'threadId': 't1', 'from': 'ann@example.com', 'to': 'owner@example.com', 'cc': '',
21
+ 'subject': 'Q3', 'date': 'D', 'messageId': '<abc@x>', 'snippet': 's', 'body': 'Hi\nthere', 'bodyType': 'text',
22
+ 'attachments': [{'filename': 'deck.pdf', 'mimeType': 'application/pdf', 'size': 12, 'attachmentId': 'A1'}],
23
+ 'labelIds': ['INBOX']}
24
+ class H(BaseHTTPRequestHandler):
25
+ def do_GET(self):
26
+ open(LOG, 'w').write(json.dumps({'method': 'GET', 'path': self.path}))
27
+ if self.path.endswith('/missing'):
28
+ self.send_response(404); self.send_header('Content-Type', 'application/json'); self.end_headers()
29
+ self.wfile.write(json.dumps({'success': False, 'error': 'google_error', 'message': 'Requested entity was not found.', 'hint': 'Google or Crewly Cloud failed; retry later.'}).encode()); return
30
+ if self.path.endswith('/no-grant'):
31
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
32
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
33
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
34
+ self.wfile.write(json.dumps({'success': True, 'data': MSG}).encode())
35
+ def log_message(self, *a, **k): pass
36
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
37
+ PY
38
+ STUB_PID=$!
39
+ disown "$STUB_PID" 2>/dev/null || true
40
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
41
+ 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
42
+
43
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
44
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
45
+
46
+ WANT='{"id":"m1","threadId":"t1","from":"ann@example.com","to":"owner@example.com","cc":"","subject":"Q3","date":"D","messageId":"<abc@x>","body":"Hi\nthere","bodyType":"text","attachments":[{"filename":"deck.pdf","mimeType":"application/pdf","size":12,"attachmentId":"A1"}]}'
47
+
48
+ OUT=$(run --id m1)
49
+ check "flags: output (no snippet/labelIds)" "$OUT" "$WANT"
50
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/google/gmail/messages/m1"
51
+
52
+ OUT=$(run '{"id":"m 1"}')
53
+ check "json: output" "$OUT" "$WANT"
54
+ check "json: id is URL-encoded" "$(jq -r .path "$STUB_LOG")" "/api/google/gmail/messages/m%201"
55
+
56
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --id missing 2>/dev/null); RC=$?
57
+ check "404: exit code" "$RC" "1"
58
+ check "404: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "google_error"
59
+ check "404: message" "$(printf '%s' "$OUT" | jq -r .message)" "Requested entity was not found."
60
+
61
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --id no-grant 2>/dev/null)
62
+ check "409: hint is the connect URL" "$(printf '%s' "$OUT" | jq -r .hint)" "https://cloud/start?token=j"
63
+
64
+ check "missing id errors" "$(run_err)" "Missing required parameter: id (--id)"
65
+ check "unknown option errors" "$(run_err --bogus 1)" "Unknown option: --bogus"
66
+
67
+ echo "=== Results: $PASS passed, $FAIL failed ==="
68
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: Gmail Search
3
+ description: Search the owner's Gmail (via the Google Workspace grant held by Crewly Cloud) and list matching messages with sender, subject, date and snippet. 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
+ - search gmail
18
+ - search email
19
+ - find email
20
+ - check inbox
21
+ tags:
22
+ - google
23
+ - gmail
24
+ - email
25
+ execution:
26
+ type: script
27
+ script:
28
+ file: execute.sh
29
+ interpreter: bash
30
+ timeoutMs: 30000
31
+ ---
32
+
33
+ # Gmail Search
34
+
35
+ Runs a Gmail search on the owner's mailbox and prints the hits. Uses the
36
+ Google Workspace connection the owner made under Settings → Integrations;
37
+ the mail content goes straight from Google to this instance.
38
+
39
+ ```bash
40
+ bash execute.sh --query "is:unread from:ann@example.com newer_than:2d" --max 10
41
+ bash execute.sh '{"query":"subject:invoice has:attachment","max":5}'
42
+ ```
43
+
44
+ ## Parameters
45
+
46
+ | Name | Required | Meaning |
47
+ |---|---|---|
48
+ | `--query` / `-q` (`query`) | yes | Gmail search syntax (`from:`, `subject:`, `is:unread`, `newer_than:7d`, `has:attachment`, …) |
49
+ | `--max` / `-n` (`max`) | no | Result cap, default 20, max 100 |
50
+
51
+ ## Output
52
+
53
+ ```json
54
+ {"query":"is:unread","count":2,"messages":[{"id":"18f…","threadId":"18f…","from":"Ann <ann@example.com>","to":"owner@example.com","subject":"Q3 numbers","date":"Thu, 18 Sep 2026 10:00:00 +0000","snippet":"Here are the…"}]}
55
+ ```
56
+
57
+ Use the `id` with `gmail-read` to get the body; `threadId` + `gmail-read`'s
58
+ `messageId` with `gmail-send` to reply in-thread.
59
+
60
+ ## Failures
61
+
62
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` — the
63
+ owner has not connected Google Workspace yet; hand them the hint URL (or
64
+ point them to Settings → Integrations → Google Workspace). Exit code 1.
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # gmail-search — Search the owner's Gmail (read-only)
4
+ #
5
+ # Backed by GET /api/google/gmail/search?q=&max= (Cloud holds the Google
6
+ # grant; this instance talks to Gmail directly).
7
+ #
8
+ # Usage:
9
+ # bash execute.sh --query "is:unread from:ann" [--max 10]
10
+ # bash execute.sh '{"query":"is:unread","max":10}'
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 --query "is:unread from:ann" [--max 10]
20
+ bash execute.sh '{"query":"is:unread","max":10}'
21
+
22
+ Options:
23
+ --query | -q Gmail search query (required)
24
+ --max | -n Result cap (default 20, max 100)
25
+ --help | -h Show this help
26
+ EOF_USAGE
27
+ }
28
+
29
+ INPUT_JSON=""
30
+ QUERY=""
31
+ MAX=""
32
+
33
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
34
+ INPUT_JSON="$1"
35
+ shift || true
36
+ fi
37
+
38
+ while [[ $# -gt 0 ]]; do
39
+ case "$1" in
40
+ --query|-q) [ $# -ge 2 ] || error_exit "--query requires a value"; QUERY="$2"; shift 2 ;;
41
+ --max|-n) [ $# -ge 2 ] || error_exit "--max requires a value"; MAX="$2"; shift 2 ;;
42
+ --help|-h) print_usage; exit 0 ;;
43
+ *) error_exit "Unknown option: $1" ;;
44
+ esac
45
+ done
46
+
47
+ if [ -n "$INPUT_JSON" ]; then
48
+ INPUT=$(read_json_input "$INPUT_JSON")
49
+ [ -z "$QUERY" ] && QUERY=$(printf '%s' "$INPUT" | jq -r '.query // .q // empty')
50
+ [ -z "$MAX" ] && MAX=$(printf '%s' "$INPUT" | jq -r '.max // empty')
51
+ fi
52
+
53
+ require_param "query (--query)" "$QUERY"
54
+
55
+ Q_ENC=$(jq -rn --arg v "$QUERY" '$v|@uri')
56
+ ENDPOINT="/google/gmail/search?q=${Q_ENC}"
57
+ [ -n "$MAX" ] && ENDPOINT="${ENDPOINT}&max=$(jq -rn --arg v "$MAX" '$v|@uri')"
58
+
59
+ RESPONSE=$(api_call GET "$ENDPOINT" 2>&1) || {
60
+ printf '%s' "$RESPONSE" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
61
+ || jq -n --arg r "$RESPONSE" '{success: false, reason: $r}'
62
+ exit 1
63
+ }
64
+
65
+ # Oversized bodies are parked on disk by api_call; pass that envelope through.
66
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
67
+ printf '%s\n' "$RESPONSE"; exit 0
68
+ fi
69
+
70
+ printf '%s' "$RESPONSE" | jq -c '{query: .data.query, count: .data.count, messages: [.data.messages[] | {id, threadId, from, to, subject, date, snippet}]}'
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # Tests for gmail-search — 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=18801
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
+ HIT = {'id': 'm1', 'threadId': 't1', 'from': 'Ann <ann@example.com>', 'to': 'owner@example.com',
22
+ 'subject': 'Q3 numbers', 'date': 'D', 'snippet': 'hi', 'labelIds': ['INBOX']}
23
+ class H(BaseHTTPRequestHandler):
24
+ def do_GET(self):
25
+ open(LOG, 'w').write(json.dumps({'method': 'GET', 'path': self.path}))
26
+ qs = parse_qs(urlparse(self.path).query)
27
+ q = qs.get('q', [''])[0]
28
+ if q == 'no-grant':
29
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
30
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant',
31
+ 'hint': 'https://api.crewlyai.com/api/cloud/google/workspace/start?token=j'}).encode()); return
32
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
33
+ self.wfile.write(json.dumps({'success': True, 'data': {'query': q, 'count': 1, 'messages': [HIT]}}).encode())
34
+ def log_message(self, *a, **k): pass
35
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
36
+ PY
37
+ STUB_PID=$!
38
+ disown "$STUB_PID" 2>/dev/null || true
39
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
40
+ 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
41
+
42
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
43
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
44
+
45
+ WANT='{"query":"is:unread from:ann","count":1,"messages":[{"id":"m1","threadId":"t1","from":"Ann <ann@example.com>","to":"owner@example.com","subject":"Q3 numbers","date":"D","snippet":"hi"}]}'
46
+
47
+ # --- flags ---
48
+ OUT=$(run --query "is:unread from:ann" --max 5)
49
+ check "flags: output drops labelIds" "$OUT" "$WANT"
50
+ check "flags: path is URL-encoded with max" "$(jq -r .path "$STUB_LOG")" "/api/google/gmail/search?q=is%3Aunread%20from%3Aann&max=5"
51
+
52
+ # --- JSON input ---
53
+ OUT=$(run '{"query":"is:unread from:ann"}')
54
+ check "json: output" "$OUT" "$WANT"
55
+ check "json: no max when absent" "$(jq -r .path "$STUB_LOG")" "/api/google/gmail/search?q=is%3Aunread%20from%3Aann"
56
+
57
+ # --- not connected → success:false + hint, exit 1 ---
58
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --query no-grant 2>/dev/null); RC=$?
59
+ check "409: exit code" "$RC" "1"
60
+ check "409: reason + hint" "$OUT" '{"success":false,"reason":"not_connected","hint":"https://api.crewlyai.com/api/cloud/google/workspace/start?token=j","message":"no grant"}'
61
+
62
+ # --- validation ---
63
+ check "missing query errors" "$(run_err --max 3)" "Missing required parameter: query (--query)"
64
+ check "unknown option errors" "$(run_err --bogus 1)" "Unknown option: --bogus"
65
+
66
+ echo "=== Results: $PASS passed, $FAIL failed ==="
67
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: Gmail Send
3
+ description: Send an email from the owner's Gmail account (via the Google Workspace grant held by Crewly Cloud). Sends on the owner's behalf — needs the owner's go-ahead unless the task brief grants it. Supports in-thread replies and a dry-run preview.
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
+ - send email
17
+ - reply to email
18
+ - email someone
19
+ tags:
20
+ - google
21
+ - gmail
22
+ - email
23
+ execution:
24
+ type: script
25
+ script:
26
+ file: execute.sh
27
+ interpreter: bash
28
+ timeoutMs: 30000
29
+ ---
30
+
31
+ # Gmail Send
32
+
33
+ Sends a plain-text email **as the owner**. Because the message carries the
34
+ owner's name, only send when the owner has said so in this conversation or
35
+ the task brief explicitly grants sending; otherwise draft with `--dry-run`
36
+ and show the preview.
37
+
38
+ ```bash
39
+ bash execute.sh --to ann@example.com --subject "Re: Q3 numbers" --text "Looks good, thanks!"
40
+ bash execute.sh --to ann@example.com --subject "Re: Q3 numbers" --text-file /tmp/reply.txt \
41
+ --thread-id 18f0a1b2c3d4e5f6 --in-reply-to "<abc@mail.example.com>"
42
+ bash execute.sh '{"to":"ann@example.com","subject":"Hi","text":"…"}'
43
+ ```
44
+
45
+ ## Parameters
46
+
47
+ | Name | Required | Meaning |
48
+ |---|---|---|
49
+ | `--to` (`to`) | yes | Recipient(s), comma-separated |
50
+ | `--subject` (`subject`) | yes | Subject (UTF-8 fine) |
51
+ | `--text` / `--text-file` (`text`) | yes | Plain-text body, inline or from a file |
52
+ | `--cc` (`cc`) | no | Cc recipient(s) |
53
+ | `--thread-id` (`threadId`) | no | Gmail thread to reply in (from `gmail-search`) |
54
+ | `--in-reply-to` (`inReplyTo`) | no | `messageId` of the message being answered (from `gmail-read`) |
55
+ | `--dry-run` | no | Print the message preview; send nothing |
56
+
57
+ `CREWLY_GMAIL_SEND_DRY_RUN=1` in the environment forces dry-run for every
58
+ call — nothing is sent, no request leaves this machine.
59
+
60
+ ## Output
61
+
62
+ Sent: `{"success":true,"id":"18f…","threadId":"18f…"}`
63
+
64
+ Dry run: `{"success":true,"dryRun":true,"preview":"To: …\nSubject: …\n\n<body>"}`
65
+
66
+ ## Failures
67
+
68
+ Missing `--to` / `--subject` / body → `{"error":"Missing required parameter: …"}` on stderr, exit 1, nothing sent.
69
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1) when the owner has not connected Google Workspace.
@@ -0,0 +1,114 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # gmail-send — Send an email from the owner's Gmail
4
+ #
5
+ # Backed by POST /api/google/gmail/send. Refuses without --to and --subject.
6
+ # With --dry-run or CREWLY_GMAIL_SEND_DRY_RUN=1 nothing is sent and no
7
+ # request is made: the message preview is printed instead.
8
+ #
9
+ # Usage:
10
+ # bash execute.sh --to a@b.c --subject "Hi" --text "Hello" [--cc x@y.z]
11
+ # [--thread-id <id>] [--in-reply-to "<msgid>"] [--dry-run]
12
+ # bash execute.sh '{"to":"a@b.c","subject":"Hi","text":"Hello"}'
13
+ # =============================================================================
14
+ set -euo pipefail
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
17
+
18
+ print_usage() {
19
+ cat <<'EOF_USAGE'
20
+ Usage:
21
+ bash execute.sh --to a@b.c --subject "Hi" --text "Hello" [--cc x@y.z]
22
+ [--thread-id <gmail thread id>] [--in-reply-to "<message-id>"] [--dry-run]
23
+ bash execute.sh '{"to":"a@b.c","subject":"Hi","text":"Hello"}'
24
+
25
+ Options:
26
+ --to Recipient(s), comma-separated (required)
27
+ --subject Subject (required)
28
+ --text Plain-text body (or --text-file <path>)
29
+ --text-file Read the body from a file
30
+ --cc Cc recipient(s)
31
+ --thread-id Reply inside this Gmail thread
32
+ --in-reply-to Message-ID being answered (sets In-Reply-To / References)
33
+ --dry-run Preview only; nothing is sent (also: CREWLY_GMAIL_SEND_DRY_RUN=1)
34
+ --help | -h Show this help
35
+ EOF_USAGE
36
+ }
37
+
38
+ INPUT_JSON=""
39
+ TO=""; CC=""; SUBJECT=""; TEXT=""; TEXT_FILE=""; THREAD_ID=""; IN_REPLY_TO=""
40
+ DRY_RUN="${CREWLY_GMAIL_SEND_DRY_RUN:-}"
41
+
42
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
43
+ INPUT_JSON="$1"
44
+ shift || true
45
+ fi
46
+
47
+ while [[ $# -gt 0 ]]; do
48
+ case "$1" in
49
+ --to) [ $# -ge 2 ] || error_exit "--to requires a value"; TO="$2"; shift 2 ;;
50
+ --cc) [ $# -ge 2 ] || error_exit "--cc requires a value"; CC="$2"; shift 2 ;;
51
+ --subject) [ $# -ge 2 ] || error_exit "--subject requires a value"; SUBJECT="$2"; shift 2 ;;
52
+ --text|--body) [ $# -ge 2 ] || error_exit "--text requires a value"; TEXT="$2"; shift 2 ;;
53
+ --text-file) [ $# -ge 2 ] || error_exit "--text-file requires a value"; TEXT_FILE="$2"; shift 2 ;;
54
+ --thread-id) [ $# -ge 2 ] || error_exit "--thread-id requires a value"; THREAD_ID="$2"; shift 2 ;;
55
+ --in-reply-to) [ $# -ge 2 ] || error_exit "--in-reply-to requires a value"; IN_REPLY_TO="$2"; shift 2 ;;
56
+ --dry-run) DRY_RUN=1; shift ;;
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 "$TO" ] && TO=$(printf '%s' "$INPUT" | jq -r '.to // empty')
65
+ [ -z "$CC" ] && CC=$(printf '%s' "$INPUT" | jq -r '.cc // empty')
66
+ [ -z "$SUBJECT" ] && SUBJECT=$(printf '%s' "$INPUT" | jq -r '.subject // empty')
67
+ [ -z "$TEXT" ] && TEXT=$(printf '%s' "$INPUT" | jq -r '.text // .body // empty')
68
+ [ -z "$THREAD_ID" ] && THREAD_ID=$(printf '%s' "$INPUT" | jq -r '.threadId // empty')
69
+ [ -z "$IN_REPLY_TO" ] && IN_REPLY_TO=$(printf '%s' "$INPUT" | jq -r '.inReplyTo // empty')
70
+ [ -z "$DRY_RUN" ] && DRY_RUN=$(printf '%s' "$INPUT" | jq -r 'if .dryRun == true then "1" else "" end')
71
+ fi
72
+
73
+ if [ -n "$TEXT_FILE" ]; then
74
+ [ -f "$TEXT_FILE" ] || error_exit "text file not found: $TEXT_FILE"
75
+ TEXT=$(cat "$TEXT_FILE")
76
+ fi
77
+
78
+ require_param "to (--to)" "$TO"
79
+ require_param "subject (--subject)" "$SUBJECT"
80
+ require_param "text (--text or --text-file)" "$TEXT"
81
+
82
+ # Dry run: no request leaves this machine. Print a readable preview of the
83
+ # headers and body exactly as they will be sent.
84
+ if [ "$DRY_RUN" = "1" ]; then
85
+ PREVIEW="To: ${TO}"
86
+ [ -n "$CC" ] && PREVIEW="${PREVIEW}
87
+ Cc: ${CC}"
88
+ PREVIEW="${PREVIEW}
89
+ Subject: ${SUBJECT}"
90
+ [ -n "$IN_REPLY_TO" ] && PREVIEW="${PREVIEW}
91
+ In-Reply-To: ${IN_REPLY_TO}"
92
+ [ -n "$THREAD_ID" ] && PREVIEW="${PREVIEW}
93
+ X-Gmail-Thread: ${THREAD_ID}"
94
+ PREVIEW="${PREVIEW}
95
+
96
+ ${TEXT}"
97
+ jq -cn --arg p "$PREVIEW" '{success: true, dryRun: true, preview: $p}'
98
+ exit 0
99
+ fi
100
+
101
+ BODY=$(jq -cn --arg to "$TO" --arg cc "$CC" --arg subject "$SUBJECT" --arg text "$TEXT" \
102
+ --arg threadId "$THREAD_ID" --arg inReplyTo "$IN_REPLY_TO" \
103
+ '{to: $to, subject: $subject, text: $text}
104
+ + (if $cc != "" then {cc: $cc} else {} end)
105
+ + (if $threadId != "" then {threadId: $threadId} else {} end)
106
+ + (if $inReplyTo != "" then {inReplyTo: $inReplyTo} else {} end)')
107
+
108
+ RESPONSE=$(api_call POST "/google/gmail/send" "$BODY" 2>&1) || {
109
+ printf '%s' "$RESPONSE" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
110
+ || jq -n --arg r "$RESPONSE" '{success: false, reason: $r}'
111
+ exit 1
112
+ }
113
+
114
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), id: .data.id, threadId: .data.threadId}'
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ # Tests for gmail-send — run with: bash execute.test.sh
3
+ # Spins up a python HTTP stub as the backend, asserts the request body the
4
+ # skill sends, the JSON it prints, and that dry-run makes NO request.
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=18803
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('to') == 'no-grant@example.com':
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
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
29
+ self.wfile.write(json.dumps({'success': True, 'data': {'id': 's1', 'threadId': data.get('threadId', 'tNew'), 'labelIds': ['SENT']}}).encode())
30
+ def log_message(self, *a, **k): pass
31
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
32
+ PY
33
+ STUB_PID=$!
34
+ disown "$STUB_PID" 2>/dev/null || true
35
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
36
+ 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
37
+
38
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_GMAIL_SEND_DRY_RUN="" bash "$EXEC" "$@" 2>/dev/null; }
39
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_GMAIL_SEND_DRY_RUN="" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
40
+
41
+ # --- send with all fields ---
42
+ OUT=$(run --to ann@example.com --cc bob@example.com --subject "Re: 第三季度" --text "Looks good" --thread-id t1 --in-reply-to "<abc@x>")
43
+ check "send: output" "$OUT" '{"success":true,"id":"s1","threadId":"t1"}'
44
+ check "send: path" "$(jq -r .path "$STUB_LOG")" "/api/google/gmail/send"
45
+ check "send: body" "$(jq -c .body "$STUB_LOG")" '{"to":"ann@example.com","subject":"Re: 第三季度","text":"Looks good","cc":"bob@example.com","threadId":"t1","inReplyTo":"<abc@x>"}'
46
+
47
+ # --- minimal + JSON input + text file ---
48
+ TMP=$(mktemp); printf 'line one\nline two' > "$TMP"
49
+ OUT=$(run '{"to":"a@b.c","subject":"Hi"}' --text-file "$TMP"); rm -f "$TMP"
50
+ check "json+file: output" "$OUT" '{"success":true,"id":"s1","threadId":"tNew"}'
51
+ check "json+file: body omits optional keys" "$(jq -c .body "$STUB_LOG")" '{"to":"a@b.c","subject":"Hi","text":"line one\nline two"}'
52
+
53
+ # --- dry run via env: no request, preview printed ---
54
+ : > "$STUB_LOG"
55
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_GMAIL_SEND_DRY_RUN=1 bash "$EXEC" --to a@b.c --cc c@d.e --subject "Hi" --text "hello" --in-reply-to "<m@x>" 2>/dev/null); RC=$?
56
+ check "env dry-run: exit 0" "$RC" "0"
57
+ check "env dry-run: preview" "$OUT" '{"success":true,"dryRun":true,"preview":"To: a@b.c\nCc: c@d.e\nSubject: Hi\nIn-Reply-To: <m@x>\n\nhello"}'
58
+ check "env dry-run: no request made" "$(cat "$STUB_LOG")" ""
59
+
60
+ # --- dry run via flag ---
61
+ OUT=$(run --to a@b.c --subject "Hi" --text "x" --dry-run)
62
+ check "flag dry-run: preview" "$OUT" '{"success":true,"dryRun":true,"preview":"To: a@b.c\nSubject: Hi\n\nx"}'
63
+ check "flag dry-run: no request made" "$(cat "$STUB_LOG")" ""
64
+
65
+ # --- dry run still validates ---
66
+ check "dry-run without --to refuses" "$(CREWLY_GMAIL_SEND_DRY_RUN=1 run_err --subject Hi --text x)" "Missing required parameter: to (--to)"
67
+
68
+ # --- backend 409 ---
69
+ OUT=$(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_GMAIL_SEND_DRY_RUN="" bash "$EXEC" --to no-grant@example.com --subject Hi --text x 2>/dev/null); RC=$?
70
+ check "409: exit code" "$RC" "1"
71
+ check "409: reason + hint" "$OUT" '{"success":false,"reason":"not_connected","hint":"https://cloud/start?token=j","message":"no grant"}'
72
+
73
+ # --- validation (nothing sent) ---
74
+ : > "$STUB_LOG"
75
+ check "missing to errors" "$(run_err --subject Hi --text x)" "Missing required parameter: to (--to)"
76
+ check "missing subject errors" "$(run_err --to a@b.c --text x)" "Missing required parameter: subject (--subject)"
77
+ check "missing text errors" "$(run_err --to a@b.c --subject Hi)" "Missing required parameter: text (--text or --text-file)"
78
+ check "missing text file errors" "$(run_err --to a@b.c --subject Hi --text-file /nonexistent/x.txt)" "text file not found: /nonexistent/x.txt"
79
+ check "validation: no request made" "$(cat "$STUB_LOG")" ""
80
+ check "unknown option errors" "$(run_err --bogus 1)" "Unknown option: --bogus"
81
+
82
+ echo "=== Results: $PASS passed, $FAIL failed ==="
83
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: List Missions
3
+ description: List the company / team / project OKRs (Missions) with their approval state and Key Result progress. Use it at registration and whenever the owner asks "what are we working towards".
4
+ version: 1.0.0
5
+ category: management
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ triggers:
11
+ - list missions
12
+ - what are our goals
13
+ - okr status
14
+ tags:
15
+ - okr
16
+ - mission
17
+ - goals
18
+ execution:
19
+ type: script
20
+ script:
21
+ file: execute.sh
22
+ interpreter: bash
23
+ timeoutMs: 15000
24
+ ---
25
+
26
+ # List Missions
27
+
28
+ Compact view of every Mission: id, level, objective, approval state, status,
29
+ parent, and the Key Results (current → target, status). Use `--full` for the
30
+ raw `GET /api/missions` payload.
31
+
32
+ ```bash
33
+ bash {{ORCHESTRATOR_SKILLS_PATH}}/list-missions/execute.sh
34
+ bash {{ORCHESTRATOR_SKILLS_PATH}}/list-missions/execute.sh --pending # only proposals awaiting the owner
35
+ ```
36
+
37
+ ## What to do with the result
38
+
39
+ - **Pending proposals** (`approval.state = pending_approval`): mention them to
40
+ the owner the next time you talk. Do not execute, remind or decompose them.
41
+ - **No missions at all** and the teams have completed work in the last week:
42
+ draft ONE company-level OKR from that work (objective + 2–3 Key Results
43
+ that can be measured from something in the system — a ticket board, a
44
+ build log, a folder count) and ask the owner whether to create it. Do not
45
+ create it yourself.
46
+ - **Live missions with off-track KRs**: the hourly sweep already alerts the
47
+ owner; you only act when a review WorkItem lands in your queue.