crewly 1.20.29 → 1.20.31

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 (112) hide show
  1. package/config/skills/agent/core/canva-create/SKILL.md +53 -0
  2. package/config/skills/agent/core/canva-create/execute.sh +88 -0
  3. package/config/skills/agent/core/canva-create/execute.test.sh +54 -0
  4. package/config/skills/agent/core/canva-designs/SKILL.md +51 -0
  5. package/config/skills/agent/core/canva-designs/execute.sh +96 -0
  6. package/config/skills/agent/core/canva-designs/execute.test.sh +54 -0
  7. package/config/skills/agent/core/canva-export/SKILL.md +52 -0
  8. package/config/skills/agent/core/canva-export/execute.sh +111 -0
  9. package/config/skills/agent/core/canva-export/execute.test.sh +55 -0
  10. package/config/skills/agent/core/canva-upload/SKILL.md +49 -0
  11. package/config/skills/agent/core/canva-upload/execute.sh +72 -0
  12. package/config/skills/agent/core/canva-upload/execute.test.sh +54 -0
  13. package/config/skills/agent/core/docs-read/SKILL.md +50 -0
  14. package/config/skills/agent/core/docs-read/execute.sh +68 -0
  15. package/config/skills/agent/core/docs-read/execute.test.sh +52 -0
  16. package/config/skills/agent/core/docs-write/SKILL.md +55 -0
  17. package/config/skills/agent/core/docs-write/execute.sh +90 -0
  18. package/config/skills/agent/core/docs-write/execute.test.sh +54 -0
  19. package/config/skills/agent/core/drive-read/SKILL.md +53 -0
  20. package/config/skills/agent/core/drive-read/execute.sh +79 -0
  21. package/config/skills/agent/core/drive-read/execute.test.sh +55 -0
  22. package/config/skills/agent/core/drive-search/SKILL.md +55 -0
  23. package/config/skills/agent/core/drive-search/execute.sh +87 -0
  24. package/config/skills/agent/core/drive-search/execute.test.sh +54 -0
  25. package/config/skills/agent/core/drive-upload/SKILL.md +54 -0
  26. package/config/skills/agent/core/drive-upload/execute.sh +117 -0
  27. package/config/skills/agent/core/drive-upload/execute.test.sh +57 -0
  28. package/config/skills/agent/core/sheets-read/SKILL.md +53 -0
  29. package/config/skills/agent/core/sheets-read/execute.sh +81 -0
  30. package/config/skills/agent/core/sheets-read/execute.test.sh +52 -0
  31. package/config/skills/agent/core/sheets-write/SKILL.md +56 -0
  32. package/config/skills/agent/core/sheets-write/execute.sh +103 -0
  33. package/config/skills/agent/core/sheets-write/execute.test.sh +57 -0
  34. package/config/skills/agent/core/slides-create/SKILL.md +61 -0
  35. package/config/skills/agent/core/slides-create/execute.sh +92 -0
  36. package/config/skills/agent/core/slides-create/execute.test.sh +54 -0
  37. package/config/skills/agent/core/slides-read/SKILL.md +49 -0
  38. package/config/skills/agent/core/slides-read/execute.sh +67 -0
  39. package/config/skills/agent/core/slides-read/execute.test.sh +50 -0
  40. package/dist/backend/backend/src/constants.d.ts +69 -0
  41. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  42. package/dist/backend/backend/src/constants.js +73 -0
  43. package/dist/backend/backend/src/constants.js.map +1 -1
  44. package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts +48 -0
  45. package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts.map +1 -0
  46. package/dist/backend/backend/src/controllers/canva/canva.controller.js +183 -0
  47. package/dist/backend/backend/src/controllers/canva/canva.controller.js.map +1 -0
  48. package/dist/backend/backend/src/controllers/canva/canva.routes.d.ts +22 -0
  49. package/dist/backend/backend/src/controllers/canva/canva.routes.d.ts.map +1 -0
  50. package/dist/backend/backend/src/controllers/canva/canva.routes.js +34 -0
  51. package/dist/backend/backend/src/controllers/canva/canva.routes.js.map +1 -0
  52. package/dist/backend/backend/src/controllers/google/google.controller.d.ts +100 -0
  53. package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
  54. package/dist/backend/backend/src/controllers/google/google.controller.js +237 -1
  55. package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
  56. package/dist/backend/backend/src/controllers/google/google.routes.d.ts +13 -0
  57. package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
  58. package/dist/backend/backend/src/controllers/google/google.routes.js +27 -1
  59. package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
  60. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  61. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  62. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  63. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  64. package/dist/backend/backend/src/services/agent/agent-registration.service.js +22 -4
  65. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  66. package/dist/backend/backend/src/services/canva/canva-token.service.d.ts +98 -0
  67. package/dist/backend/backend/src/services/canva/canva-token.service.d.ts.map +1 -0
  68. package/dist/backend/backend/src/services/canva/canva-token.service.js +198 -0
  69. package/dist/backend/backend/src/services/canva/canva-token.service.js.map +1 -0
  70. package/dist/backend/backend/src/services/canva/canva.service.d.ts +184 -0
  71. package/dist/backend/backend/src/services/canva/canva.service.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/canva/canva.service.js +274 -0
  73. package/dist/backend/backend/src/services/canva/canva.service.js.map +1 -0
  74. package/dist/backend/backend/src/services/google/docs.service.d.ts +117 -0
  75. package/dist/backend/backend/src/services/google/docs.service.d.ts.map +1 -0
  76. package/dist/backend/backend/src/services/google/docs.service.js +152 -0
  77. package/dist/backend/backend/src/services/google/docs.service.js.map +1 -0
  78. package/dist/backend/backend/src/services/google/drive.service.d.ts +157 -0
  79. package/dist/backend/backend/src/services/google/drive.service.d.ts.map +1 -0
  80. package/dist/backend/backend/src/services/google/drive.service.js +203 -0
  81. package/dist/backend/backend/src/services/google/drive.service.js.map +1 -0
  82. package/dist/backend/backend/src/services/google/google-api.client.d.ts +15 -7
  83. package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
  84. package/dist/backend/backend/src/services/google/google-api.client.js +13 -6
  85. package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
  86. package/dist/backend/backend/src/services/google/sheets.service.d.ts +118 -0
  87. package/dist/backend/backend/src/services/google/sheets.service.d.ts.map +1 -0
  88. package/dist/backend/backend/src/services/google/sheets.service.js +158 -0
  89. package/dist/backend/backend/src/services/google/sheets.service.js.map +1 -0
  90. package/dist/backend/backend/src/services/google/slides.service.d.ts +136 -0
  91. package/dist/backend/backend/src/services/google/slides.service.d.ts.map +1 -0
  92. package/dist/backend/backend/src/services/google/slides.service.js +153 -0
  93. package/dist/backend/backend/src/services/google/slides.service.js.map +1 -0
  94. package/dist/backend/backend/src/utils/tool-call-markup.utils.d.ts +45 -0
  95. package/dist/backend/backend/src/utils/tool-call-markup.utils.d.ts.map +1 -0
  96. package/dist/backend/backend/src/utils/tool-call-markup.utils.js +83 -0
  97. package/dist/backend/backend/src/utils/tool-call-markup.utils.js.map +1 -0
  98. package/dist/backend/build-info.json +2 -2
  99. package/dist/cli/backend/src/constants.d.ts +69 -0
  100. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  101. package/dist/cli/backend/src/constants.js +73 -0
  102. package/dist/cli/backend/src/constants.js.map +1 -1
  103. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  104. package/dist/cli/backend/src/services/agent/agent-registration.service.js +22 -4
  105. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  106. package/dist/cli/backend/src/utils/tool-call-markup.utils.d.ts +45 -0
  107. package/dist/cli/backend/src/utils/tool-call-markup.utils.d.ts.map +1 -0
  108. package/dist/cli/backend/src/utils/tool-call-markup.utils.js +83 -0
  109. package/dist/cli/backend/src/utils/tool-call-markup.utils.js.map +1 -0
  110. package/frontend/dist/assets/{index-d1bd0dec.js → index-96b51cd9.js} +243 -243
  111. package/frontend/dist/index.html +1 -1
  112. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Canva Create
3
+ description: Create a new Canva design in the owner's account — a preset (doc / whiteboard / presentation), a custom size (e.g. 1080x1920 for stories), or one built from an uploaded asset (via the Canva grant held by Crewly Cloud). Returns the edit link for a person to finish the design.
4
+ version: 1.0.0
5
+ category: productivity
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
+ - create canva design
18
+ - new design in canva
19
+ - make a poster in canva
20
+ - start a canva deck
21
+ tags:
22
+ - canva
23
+ - create
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Canva Create
33
+
34
+ ```bash
35
+ bash execute.sh --title "Open day poster" --size 1080x1350
36
+ bash execute.sh --title "Reel cover" --asset AAB… # after canva-upload
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"success":true,"design":{"id":"DAF…","title":"Open day poster","editUrl":"https://www.canva.com/…"}}
43
+ ```
44
+
45
+ The Connect API does not fill designs with text or generate video: create
46
+ the design, upload the assets, then hand the `editUrl` to a person (or
47
+ export an existing finished design with `canva-export`). Brand templates
48
+ need Canva Enterprise and are not wired.
49
+
50
+ ## Failures
51
+
52
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
53
+ when the owner has not connected Canva (Settings → Integrations).
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # canva-create — Create a new Canva design (preset type, custom size, or from an uploaded asset)
4
+ #
5
+ # Backed by POST /api/canva/designs.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --title "Open day poster" --size 1080x1350
9
+ # bash execute.sh --title "Term plan" --preset presentation # doc | whiteboard | presentation
10
+ # bash execute.sh --title "Flyer" --asset <assetId> # start from an uploaded image
11
+ # bash execute.sh '{"title":"Poster","width":1080,"height":1920}'
12
+ # =============================================================================
13
+ set -euo pipefail
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
16
+
17
+ print_usage() {
18
+ cat <<'EOF_USAGE'
19
+ Usage:
20
+ bash execute.sh --title "Open day poster" --size 1080x1350
21
+ bash execute.sh --title "Term plan" --preset presentation # doc | whiteboard | presentation
22
+ bash execute.sh --title "Flyer" --asset <assetId> # start from an uploaded image
23
+ bash execute.sh '{"title":"Poster","width":1080,"height":1920}'
24
+
25
+ Options:
26
+ --title Design title (≤ 255 chars)
27
+ --preset doc | whiteboard | presentation
28
+ --size WIDTHxHEIGHT in px (40–8000), e.g. 1080x1920 for a vertical video/story
29
+ --asset Asset id from canva-upload to place on the first page
30
+ --help | -h Show this help
31
+ EOF_USAGE
32
+ }
33
+
34
+ fail_from() {
35
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
36
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
37
+ exit 1
38
+ }
39
+
40
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
41
+
42
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
43
+ # the backend answer is always the last line. On failure print the mapped
44
+ # failure JSON (fail_from) and return 1.
45
+ call() {
46
+ local out
47
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
48
+ ' "$out" | tail -n 1)"; }
49
+ printf '%s
50
+ ' "$out" | tail -n 1
51
+ }
52
+
53
+ INPUT_JSON=""
54
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
55
+ INPUT_JSON="$1"
56
+ shift || true
57
+ fi
58
+ TITLE=""; PRESET=""; SIZE=""; WIDTH=""; HEIGHT=""; ASSET=""
59
+ while [[ $# -gt 0 ]]; do
60
+ case "$1" in
61
+ --title) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
62
+ --preset) [ $# -ge 2 ] || error_exit "--preset requires a value"; PRESET="$2"; shift 2 ;;
63
+ --size) [ $# -ge 2 ] || error_exit "--size requires a value"; SIZE="$2"; shift 2 ;;
64
+ --asset) [ $# -ge 2 ] || error_exit "--asset requires a value"; ASSET="$2"; shift 2 ;;
65
+ --help|-h) print_usage; exit 0 ;;
66
+ *) error_exit "Unknown option: $1" ;;
67
+ esac
68
+ done
69
+ if [ -n "$INPUT_JSON" ]; then
70
+ INPUT=$(read_json_input "$INPUT_JSON")
71
+ [ -z "$TITLE" ] && TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
72
+ [ -z "$PRESET" ] && PRESET=$(printf '%s' "$INPUT" | jq -r '.preset // empty')
73
+ [ -z "$WIDTH" ] && WIDTH=$(printf '%s' "$INPUT" | jq -r '.width // empty')
74
+ [ -z "$HEIGHT" ] && HEIGHT=$(printf '%s' "$INPUT" | jq -r '.height // empty')
75
+ [ -z "$ASSET" ] && ASSET=$(printf '%s' "$INPUT" | jq -r '.asset // .assetId // empty')
76
+ fi
77
+ if [ -n "$SIZE" ]; then
78
+ [[ "$SIZE" =~ ^([0-9]+)x([0-9]+)$ ]] || error_exit "--size must look like 1080x1920"
79
+ WIDTH="${BASH_REMATCH[1]}"; HEIGHT="${BASH_REMATCH[2]}"
80
+ fi
81
+ [ -n "$PRESET" ] || [ -n "$WIDTH" ] || [ -n "$ASSET" ] || error_exit "give --preset, --size or --asset"
82
+ BODY=$(jq -cn --arg title "$TITLE" --arg preset "$PRESET" --arg w "$WIDTH" --arg h "$HEIGHT" --arg asset "$ASSET" \
83
+ '{} + (if $title != "" then {title: $title} else {} end)
84
+ + (if $preset != "" then {preset: $preset} else {} end)
85
+ + (if $w != "" then {width: ($w|tonumber), height: ($h|tonumber)} else {} end)
86
+ + (if $asset != "" then {assetId: $asset} else {} end)')
87
+ RESPONSE=$(call POST "/canva/designs" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
88
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), design: .data}'
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Tests for canva-create — run with: bash execute.test.sh
3
+ # Python HTTP stub as the backend; asserts the request the skill sends and
4
+ # 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=18821
15
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
16
+ python3 -u <<'PY' >/dev/null 2>&1 &
17
+ from http.server import BaseHTTPRequestHandler, HTTPServer
18
+ import json, os
19
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
20
+ class H(BaseHTTPRequestHandler):
21
+ def _reply(self, method, data=None):
22
+ open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
23
+ if 'no-grant' in self.path or (data or {}).get('title') == 'no-grant':
24
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
25
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
26
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
27
+ self.wfile.write(json.dumps({'success': True, 'data': {'id': 'D9', 'title': data.get('title'), 'editUrl': 'https://e'}}).encode())
28
+ def do_GET(self): self._reply('GET')
29
+ def do_POST(self):
30
+ n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
31
+ self._reply('POST', json.loads(body or '{}'))
32
+ def log_message(self, *a, **k): pass
33
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
34
+ PY
35
+ STUB_PID=$!
36
+ disown "$STUB_PID" 2>/dev/null || true
37
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
38
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
39
+
40
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
41
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
42
+
43
+ OUT=$(run --title "Poster" --size 1080x1350)
44
+ check "size: output" "$OUT" '{"success":true,"design":{"id":"D9","title":"Poster","editUrl":"https://e"}}'
45
+ check "size: body" "$(jq -c .body "$STUB_LOG")" '{"title":"Poster","width":1080,"height":1350}'
46
+ run --title "Deck" --preset presentation >/dev/null
47
+ check "preset: body" "$(jq -c .body "$STUB_LOG")" '{"title":"Deck","preset":"presentation"}'
48
+ run '{"assetId":"A1"}' >/dev/null
49
+ check "asset: body" "$(jq -c .body "$STUB_LOG")" '{"assetId":"A1"}'
50
+ OUT=$(run_err --title "x" || true)
51
+ check "nothing to create fails" "$(printf '%s' "$OUT" | grep -c 'give --preset')" "1"
52
+
53
+ echo "canva-create: $PASS passed, $FAIL failed"
54
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: Canva Designs
3
+ description: List or search the owner's Canva designs, or fetch one by id — returns edit/view links and thumbnails (via the Canva grant held by Crewly Cloud). Read-only.
4
+ version: 1.0.0
5
+ category: productivity
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ - team-leader
10
+ - developer
11
+ - operations
12
+ - ops
13
+ - sales
14
+ - support
15
+ - generalist
16
+ triggers:
17
+ - list canva designs
18
+ - find the canva design
19
+ - open in canva
20
+ - which canva design
21
+ tags:
22
+ - canva
23
+ - designs
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Canva Designs
33
+
34
+ ```bash
35
+ bash execute.sh --query "open day poster" --owned
36
+ bash execute.sh --id DAF…
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"count":1,"designs":[{"id":"DAF…","title":"Open day poster","editUrl":"https://www.canva.com/…","viewUrl":"https://…","thumbnailUrl":"https://…","pageCount":1,"updatedAt":"…"}]}
43
+ ```
44
+
45
+ `editUrl` / `viewUrl` are valid for 30 days, `thumbnailUrl` for 15 minutes —
46
+ share the edit link with people, do not store the thumbnail.
47
+
48
+ ## Failures
49
+
50
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
51
+ when the owner has not connected Canva (Settings → Integrations).
@@ -0,0 +1,96 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # canva-designs — List / search the owner's Canva designs, or fetch one (edit + view links)
4
+ #
5
+ # Backed by GET /api/canva/designs and GET /api/canva/designs/:id.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh [--query "poster"] [--owned|--shared] [--sort modified_descending] [--limit 25]
9
+ # bash execute.sh --id <designId>
10
+ # bash execute.sh '{"query":"poster","limit":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 "poster"] [--owned|--shared] [--sort modified_descending] [--limit 25]
20
+ bash execute.sh --id <designId>
21
+ bash execute.sh '{"query":"poster","limit":10}'
22
+
23
+ Options:
24
+ --query, -q Text to search design titles
25
+ --id Fetch one design instead of listing
26
+ --owned Only designs the owner created (default: any)
27
+ --shared Only designs shared with the owner
28
+ --sort relevance | modified_descending | modified_ascending | title_descending | title_ascending
29
+ --limit Result cap (default 25, max 100)
30
+ --continuation Token from a previous page
31
+ --help | -h Show this help
32
+ EOF_USAGE
33
+ }
34
+
35
+ fail_from() {
36
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
37
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
38
+ exit 1
39
+ }
40
+
41
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
42
+
43
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
44
+ # the backend answer is always the last line. On failure print the mapped
45
+ # failure JSON (fail_from) and return 1.
46
+ call() {
47
+ local out
48
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
49
+ ' "$out" | tail -n 1)"; }
50
+ printf '%s
51
+ ' "$out" | tail -n 1
52
+ }
53
+
54
+ INPUT_JSON=""
55
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
56
+ INPUT_JSON="$1"
57
+ shift || true
58
+ fi
59
+ QUERY=""; ID=""; OWNERSHIP=""; SORT=""; LIMIT=""; CONT=""
60
+ while [[ $# -gt 0 ]]; do
61
+ case "$1" in
62
+ --query|-q) [ $# -ge 2 ] || error_exit "--query requires a value"; QUERY="$2"; shift 2 ;;
63
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
64
+ --owned) OWNERSHIP="owned"; shift ;;
65
+ --shared) OWNERSHIP="shared"; shift ;;
66
+ --sort) [ $# -ge 2 ] || error_exit "--sort requires a value"; SORT="$2"; shift 2 ;;
67
+ --limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
68
+ --continuation) [ $# -ge 2 ] || error_exit "--continuation requires a value"; CONT="$2"; shift 2 ;;
69
+ --help|-h) print_usage; exit 0 ;;
70
+ *) error_exit "Unknown option: $1" ;;
71
+ esac
72
+ done
73
+ if [ -n "$INPUT_JSON" ]; then
74
+ INPUT=$(read_json_input "$INPUT_JSON")
75
+ [ -z "$QUERY" ] && QUERY=$(printf '%s' "$INPUT" | jq -r '.query // .q // empty')
76
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .designId // empty')
77
+ [ -z "$OWNERSHIP" ] && OWNERSHIP=$(printf '%s' "$INPUT" | jq -r '.ownership // empty')
78
+ [ -z "$SORT" ] && SORT=$(printf '%s' "$INPUT" | jq -r '.sort // .sortBy // empty')
79
+ [ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
80
+ [ -z "$CONT" ] && CONT=$(printf '%s' "$INPUT" | jq -r '.continuation // empty')
81
+ fi
82
+ if [ -n "$ID" ]; then
83
+ RESPONSE=$(call GET "/canva/designs/$(uri "$ID")") || { printf '%s\n' "$RESPONSE"; exit 1; }
84
+ printf '%s' "$RESPONSE" | jq -c '.data'
85
+ exit 0
86
+ fi
87
+ QS=""
88
+ [ -n "$QUERY" ] && QS="${QS}&q=$(uri "$QUERY")"
89
+ [ -n "$OWNERSHIP" ] && QS="${QS}&ownership=$(uri "$OWNERSHIP")"
90
+ [ -n "$SORT" ] && QS="${QS}&sort=$(uri "$SORT")"
91
+ [ -n "$LIMIT" ] && QS="${QS}&limit=$(uri "$LIMIT")"
92
+ [ -n "$CONT" ] && QS="${QS}&continuation=$(uri "$CONT")"
93
+ QS="${QS#&}"
94
+ RESPONSE=$(call GET "/canva/designs${QS:+?$QS}") || { printf '%s\n' "$RESPONSE"; exit 1; }
95
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
96
+ printf '%s' "$RESPONSE" | jq -c '{count: .data.count, designs: .data.designs} + (if .data.continuation then {continuation: .data.continuation} else {} end)'
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Tests for canva-designs — run with: bash execute.test.sh
3
+ # Python HTTP stub as the backend; asserts the request the skill sends and
4
+ # 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=18820
15
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
16
+ python3 -u <<'PY' >/dev/null 2>&1 &
17
+ from http.server import BaseHTTPRequestHandler, HTTPServer
18
+ import json, os
19
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
20
+ class H(BaseHTTPRequestHandler):
21
+ def _reply(self, method, data=None):
22
+ open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
23
+ if 'no-grant' in self.path or (data or {}).get('title') == 'no-grant':
24
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
25
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
26
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
27
+ self.wfile.write(json.dumps({'success': True, 'data': {'count': 1, 'designs': [{'id': 'D1', 'title': 'Poster', 'editUrl': 'https://e'}], 'id': 'D1', 'title': 'Poster', 'editUrl': 'https://e'}}).encode())
28
+ def do_GET(self): self._reply('GET')
29
+ def do_POST(self):
30
+ n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
31
+ self._reply('POST', json.loads(body or '{}'))
32
+ def log_message(self, *a, **k): pass
33
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
34
+ PY
35
+ STUB_PID=$!
36
+ disown "$STUB_PID" 2>/dev/null || true
37
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
38
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
39
+
40
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
41
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
42
+
43
+ OUT=$(run --query "open day" --owned --sort modified_descending --limit 5)
44
+ check "list: output" "$OUT" '{"count":1,"designs":[{"id":"D1","title":"Poster","editUrl":"https://e"}]}'
45
+ check "list: path" "$(jq -r .path "$STUB_LOG")" "/api/canva/designs?q=open%20day&ownership=owned&sort=modified_descending&limit=5"
46
+ run --id D1 >/dev/null
47
+ check "get: path" "$(jq -r .path "$STUB_LOG")" "/api/canva/designs/D1"
48
+ run '{"shared":false}' >/dev/null
49
+ check "json: bare list" "$(jq -r .path "$STUB_LOG")" "/api/canva/designs"
50
+ OUT=$(run --query no-grant || true)
51
+ check "not connected" "$(printf '%s' "$OUT" | jq -r .reason)" "not_connected"
52
+
53
+ echo "canva-designs: $PASS passed, $FAIL failed"
54
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: Canva Export
3
+ description: Export a Canva design to PDF, PNG, JPG, PPTX, GIF or MP4 (via the Canva grant held by Crewly Cloud). Waits for Canva's export job and returns download URLs; with --out / --out-dir the files are downloaded. Use this to turn a finished Canva video/poster into a file you can post or send.
4
+ version: 1.0.0
5
+ category: productivity
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
+ - export from canva
18
+ - download the canva design
19
+ - render the canva video
20
+ - canva to pdf
21
+ tags:
22
+ - canva
23
+ - export
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Canva Export
33
+
34
+ ```bash
35
+ bash execute.sh --id DAF… --format mp4 --out /tmp/reel.mp4
36
+ bash execute.sh --id DAF… --format png --out-dir /tmp/poster-pages
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"success":true,"jobId":"…","urls":["https://export-download.canva.com/…"],"savedTo":["/tmp/reel.mp4"]}
43
+ ```
44
+
45
+ Download URLs are short-lived — save the file right away. Exports of
46
+ large videos can take a minute or two; the backend polls for up to 2 min
47
+ and answers `504` if Canva is still rendering (retry later).
48
+
49
+ ## Failures
50
+
51
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
52
+ when the owner has not connected Canva (Settings → Integrations).
@@ -0,0 +1,111 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # canva-export — Export a Canva design to PDF / PNG / JPG / PPTX / GIF / MP4 and download it
4
+ #
5
+ # Backed by POST /api/canva/designs/:id/export (async job, polled by the backend).
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --id <designId> --format pdf [--out /tmp/poster.pdf]
9
+ # bash execute.sh --id <designId> --format mp4 [--video-quality horizontal_1080p] --out /tmp/reel.mp4
10
+ # bash execute.sh --id <designId> --format png --pages 1,2 --out-dir /tmp/pages
11
+ # bash execute.sh '{"id":"<designId>","format":"jpg","quality":85}'
12
+ # =============================================================================
13
+ set -euo pipefail
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
16
+
17
+ print_usage() {
18
+ cat <<'EOF_USAGE'
19
+ Usage:
20
+ bash execute.sh --id <designId> --format pdf [--out /tmp/poster.pdf]
21
+ bash execute.sh --id <designId> --format mp4 [--video-quality horizontal_1080p] --out /tmp/reel.mp4
22
+ bash execute.sh --id <designId> --format png --pages 1,2 --out-dir /tmp/pages
23
+ bash execute.sh '{"id":"<designId>","format":"jpg","quality":85}'
24
+
25
+ Options:
26
+ --id Design id (required)
27
+ --format pdf | png | jpg | pptx | gif | mp4 (required)
28
+ --quality JPG quality 1–100 (default 80)
29
+ --video-quality MP4 preset: horizontal_480p|720p|1080p|4k, vertical_… (default horizontal_1080p)
30
+ --pages Comma-separated 1-based page numbers (default all)
31
+ --out Download the (first) file to this path
32
+ --out-dir Download every file into this directory (page-1.png, page-2.png, …)
33
+ --help | -h Show this help
34
+ EOF_USAGE
35
+ }
36
+
37
+ fail_from() {
38
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
39
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
40
+ exit 1
41
+ }
42
+
43
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
44
+
45
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
46
+ # the backend answer is always the last line. On failure print the mapped
47
+ # failure JSON (fail_from) and return 1.
48
+ call() {
49
+ local out
50
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
51
+ ' "$out" | tail -n 1)"; }
52
+ printf '%s
53
+ ' "$out" | tail -n 1
54
+ }
55
+
56
+ INPUT_JSON=""
57
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
58
+ INPUT_JSON="$1"
59
+ shift || true
60
+ fi
61
+ ID=""; FORMAT=""; QUALITY=""; VQ=""; PAGES=""; OUT_PATH=""; OUT_DIR=""
62
+ while [[ $# -gt 0 ]]; do
63
+ case "$1" in
64
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
65
+ --format|-f) [ $# -ge 2 ] || error_exit "--format requires a value"; FORMAT="$2"; shift 2 ;;
66
+ --quality) [ $# -ge 2 ] || error_exit "--quality requires a value"; QUALITY="$2"; shift 2 ;;
67
+ --video-quality) [ $# -ge 2 ] || error_exit "--video-quality requires a value"; VQ="$2"; shift 2 ;;
68
+ --pages) [ $# -ge 2 ] || error_exit "--pages requires a value"; PAGES="$2"; shift 2 ;;
69
+ --out|-o) [ $# -ge 2 ] || error_exit "--out requires a value"; OUT_PATH="$2"; shift 2 ;;
70
+ --out-dir) [ $# -ge 2 ] || error_exit "--out-dir requires a value"; OUT_DIR="$2"; shift 2 ;;
71
+ --help|-h) print_usage; exit 0 ;;
72
+ *) error_exit "Unknown option: $1" ;;
73
+ esac
74
+ done
75
+ if [ -n "$INPUT_JSON" ]; then
76
+ INPUT=$(read_json_input "$INPUT_JSON")
77
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .designId // empty')
78
+ [ -z "$FORMAT" ] && FORMAT=$(printf '%s' "$INPUT" | jq -r '.format // empty')
79
+ [ -z "$QUALITY" ] && QUALITY=$(printf '%s' "$INPUT" | jq -r '.quality // empty')
80
+ [ -z "$VQ" ] && VQ=$(printf '%s' "$INPUT" | jq -r '.videoQuality // empty')
81
+ [ -z "$PAGES" ] && PAGES=$(printf '%s' "$INPUT" | jq -r '(.pages // []) | map(tostring) | join(",")')
82
+ [ -z "$OUT_PATH" ] && OUT_PATH=$(printf '%s' "$INPUT" | jq -r '.out // empty')
83
+ [ -z "$OUT_DIR" ] && OUT_DIR=$(printf '%s' "$INPUT" | jq -r '.outDir // empty')
84
+ fi
85
+ require_param "id (--id)" "$ID"
86
+ require_param "format (--format)" "$FORMAT"
87
+ BODY=$(jq -cn --arg format "$FORMAT" --arg q "$QUALITY" --arg vq "$VQ" --arg pages "$PAGES" \
88
+ '{format: $format}
89
+ + (if $q != "" then {quality: ($q|tonumber)} else {} end)
90
+ + (if $vq != "" then {videoQuality: $vq} else {} end)
91
+ + (if $pages != "" then {pages: ($pages | split(",") | map(tonumber))} else {} end)')
92
+ RESPONSE=$(call POST "/canva/designs/$(uri "$ID")/export" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
93
+ STATUS=$(printf '%s' "$RESPONSE" | jq -r '.data.status // "failed"')
94
+ if [ "$STATUS" != "success" ]; then
95
+ printf '%s' "$RESPONSE" | jq -c '{success: false, reason: "export_failed", status: .data.status, error: .data.error}'
96
+ exit 1
97
+ fi
98
+ SAVED="[]"
99
+ if [ -n "$OUT_PATH" ] || [ -n "$OUT_DIR" ]; then
100
+ i=0
101
+ while IFS= read -r url; do
102
+ [ -n "$url" ] || continue
103
+ i=$((i+1))
104
+ if [ -n "$OUT_PATH" ] && [ "$i" -eq 1 ]; then dest="$OUT_PATH"
105
+ elif [ -n "$OUT_DIR" ]; then mkdir -p "$OUT_DIR"; dest="$OUT_DIR/page-$i.$FORMAT"
106
+ else continue; fi
107
+ curl -sfL -o "$dest" "$url" || error_exit "download failed: $url"
108
+ SAVED=$(printf '%s' "$SAVED" | jq -c --arg d "$dest" '. + [$d]')
109
+ done < <(printf '%s' "$RESPONSE" | jq -r '.data.urls[]')
110
+ fi
111
+ printf '%s' "$RESPONSE" | jq -c --argjson saved "$SAVED" '{success: true, jobId: .data.jobId, urls: .data.urls, savedTo: $saved}'
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # Tests for canva-export — run with: bash execute.test.sh
3
+ # Python HTTP stub as the backend; asserts the request the skill sends and
4
+ # 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=18822
15
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
16
+ python3 -u <<'PY' >/dev/null 2>&1 &
17
+ from http.server import BaseHTTPRequestHandler, HTTPServer
18
+ import json, os
19
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
20
+ class H(BaseHTTPRequestHandler):
21
+ def _reply(self, method, data=None):
22
+ open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
23
+ if 'no-grant' in self.path or (data or {}).get('title') == 'no-grant':
24
+ self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
25
+ self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
26
+ self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
27
+ self.wfile.write(json.dumps({'success': True, 'data': {'jobId': 'J1', 'status': 'success', 'urls': ['http://127.0.0.1:%d/dl/1' % PORT]}}).encode())
28
+ def do_GET(self): self._reply('GET')
29
+ def do_POST(self):
30
+ n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
31
+ self._reply('POST', json.loads(body or '{}'))
32
+ def log_message(self, *a, **k): pass
33
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
34
+ PY
35
+ STUB_PID=$!
36
+ disown "$STUB_PID" 2>/dev/null || true
37
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
38
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
39
+
40
+ run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
41
+ run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
42
+
43
+ OUT=$(run --id D1 --format pdf --pages 1,2)
44
+ check "pdf: output" "$OUT" "{\"success\":true,\"jobId\":\"J1\",\"urls\":[\"http://127.0.0.1:${PORT}/dl/1\"],\"savedTo\":[]}"
45
+ check "pdf: path+body" "$(jq -r '.path + " " + (.body|tojson)' "$STUB_LOG")" '/api/canva/designs/D1/export {"format":"pdf","pages":[1,2]}'
46
+ run '{"id":"D1","format":"jpg","quality":85}' >/dev/null
47
+ check "jpg: body" "$(jq -c .body "$STUB_LOG")" '{"format":"jpg","quality":85}'
48
+ TMP="$(mktemp).pdf"
49
+ OUT=$(run --id D1 --format pdf --out "$TMP")
50
+ check "download: saved" "$(printf '%s' "$OUT" | jq -r '.savedTo[0]')" "$TMP"
51
+ check "download: bytes" "$(cat "$TMP")" '{"success": true, "data": {"jobId": "J1", "status": "success", "urls": ["http://127.0.0.1:'"$PORT"'/dl/1"]}}'
52
+ rm -f "$TMP"
53
+
54
+ echo "canva-export: $PASS passed, $FAIL failed"
55
+ [ "$FAIL" -eq 0 ]