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
|
@@ -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.
|