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,49 @@
1
+ ---
2
+ name: Canva Upload
3
+ description: Upload a local image or video into the owner's Canva media library as an asset (via the Canva grant held by Crewly Cloud). The asset id can seed a new design with canva-create.
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
+ - upload to canva
18
+ - add image to canva
19
+ - put this video in canva
20
+ tags:
21
+ - canva
22
+ - upload
23
+ execution:
24
+ type: script
25
+ script:
26
+ file: execute.sh
27
+ interpreter: bash
28
+ timeoutMs: 60000
29
+ ---
30
+
31
+ # Canva Upload
32
+
33
+ ```bash
34
+ bash execute.sh --path ./open-day.jpg --name "Open day photo"
35
+ ```
36
+
37
+ ## Output
38
+
39
+ ```json
40
+ {"success":true,"asset":{"id":"AAB…","name":"Open day photo","thumbnailUrl":"https://…"}}
41
+ ```
42
+
43
+ Limit 50 MB per file. Then `canva-create --asset <id>` starts a design
44
+ with it, or a person drops it into an existing design in Canva.
45
+
46
+ ## Failures
47
+
48
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
49
+ when the owner has not connected Canva (Settings → Integrations).
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # canva-upload — Upload an image or video to the owner's Canva as an asset
4
+ #
5
+ # Backed by POST /api/canva/assets (base64 body; the backend polls the Canva upload job).
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --path ./logo.png [--name "School logo"]
9
+ # bash execute.sh '{"path":"./clip.mp4","name":"Open day clip"}'
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 --path ./logo.png [--name "School logo"]
19
+ bash execute.sh '{"path":"./clip.mp4","name":"Open day clip"}'
20
+
21
+ Options:
22
+ --path, -p Local image / video file (≤ 50 MB). (Not --file: reserved by the skill runner.)
23
+ --name Asset name in Canva (≤ 50 chars; default: the file's basename)
24
+ --help | -h Show this help
25
+ EOF_USAGE
26
+ }
27
+
28
+ fail_from() {
29
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
30
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
31
+ exit 1
32
+ }
33
+
34
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
35
+
36
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
37
+ # the backend answer is always the last line. On failure print the mapped
38
+ # failure JSON (fail_from) and return 1.
39
+ call() {
40
+ local out
41
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
42
+ ' "$out" | tail -n 1)"; }
43
+ printf '%s
44
+ ' "$out" | tail -n 1
45
+ }
46
+
47
+ INPUT_JSON=""
48
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
49
+ INPUT_JSON="$1"
50
+ shift || true
51
+ fi
52
+ FILE=""; NAME=""
53
+ while [[ $# -gt 0 ]]; do
54
+ case "$1" in
55
+ --path|-p) [ $# -ge 2 ] || error_exit "--path requires a value"; FILE="$2"; shift 2 ;;
56
+ --name) [ $# -ge 2 ] || error_exit "--name requires a value"; NAME="$2"; shift 2 ;;
57
+ --help|-h) print_usage; exit 0 ;;
58
+ *) error_exit "Unknown option: $1" ;;
59
+ esac
60
+ done
61
+ if [ -n "$INPUT_JSON" ]; then
62
+ INPUT=$(read_json_input "$INPUT_JSON")
63
+ [ -z "$FILE" ] && FILE=$(printf '%s' "$INPUT" | jq -r '.path // .file // empty')
64
+ [ -z "$NAME" ] && NAME=$(printf '%s' "$INPUT" | jq -r '.name // empty')
65
+ fi
66
+ require_param "path (--path)" "$FILE"
67
+ [ -f "$FILE" ] || error_exit "file not found: $FILE"
68
+ [ -z "$NAME" ] && NAME=$(basename "$FILE")
69
+ CONTENT=$(base64 < "$FILE" | tr -d '\n')
70
+ BODY=$(jq -cn --arg name "$NAME" --arg content "$CONTENT" '{name: $name, content: $content}')
71
+ RESPONSE=$(call POST "/canva/assets" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
72
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), asset: .data}'
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Tests for canva-upload — 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=18823
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': 'A1', 'name': data.get('name'), 'thumbnailUrl': 'https://t'}}).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
+ BIN="$(mktemp).png"; printf '\x89PNG\x00' > "$BIN"
44
+ OUT=$(run --path "$BIN" --name "Logo")
45
+ check "upload: output" "$OUT" '{"success":true,"asset":{"id":"A1","name":"Logo","thumbnailUrl":"https://t"}}'
46
+ check "upload: body" "$(jq -c .body "$STUB_LOG")" '{"name":"Logo","content":"iVBORwA="}'
47
+ run "{\"path\":\"$BIN\"}" >/dev/null
48
+ check "json: default name" "$(jq -r .body.name "$STUB_LOG")" "$(basename "$BIN")"
49
+ rm -f "$BIN"
50
+ OUT=$(run_err --path /nonexistent || true)
51
+ check "missing file fails" "$(printf '%s' "$OUT" | grep -c 'not found')" "1"
52
+
53
+ echo "canva-upload: $PASS passed, $FAIL failed"
54
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: Docs Read
3
+ description: Read a Google Doc as plain text — headings become `#` lines, bullets `-`, table rows `a | b` (via the Google Workspace grant held by Crewly Cloud). Accepts a document id or its URL. 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
+ - read google doc
18
+ - open the doc
19
+ - what does the document say
20
+ - summarize this doc
21
+ tags:
22
+ - google
23
+ - docs
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Docs Read
33
+
34
+ ```bash
35
+ bash execute.sh --id 1AbC…
36
+ bash execute.sh --id "https://docs.google.com/document/d/1AbC…/edit"
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"id":"1AbC…","title":"Q3 plan","webViewLink":"https://docs.google.com/document/d/1AbC…/edit","text":"# Q3 plan\n\n- Ship…"}
43
+ ```
44
+
45
+ ## Failures
46
+
47
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
48
+ when the owner has not connected Google Workspace (Settings → Integrations).
49
+ A `403` with `reason: "google_error"` on a write means the file was not
50
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # docs-read — Read a Google Doc as plain text (headings, bullets, tables kept)
4
+ #
5
+ # Backed by GET /api/google/docs/:id.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --id <documentId>
9
+ # bash execute.sh '{"id":"<documentId>"}'
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 <documentId>
19
+ bash execute.sh '{"id":"<documentId>"}'
20
+
21
+ Options:
22
+ --id Document id (from drive-search or the docs.google.com URL)
23
+ --help | -h Show this help
24
+ EOF_USAGE
25
+ }
26
+
27
+ fail_from() {
28
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
29
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
30
+ exit 1
31
+ }
32
+
33
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
34
+
35
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
36
+ # the backend answer is always the last line. On failure print the mapped
37
+ # failure JSON (fail_from) and return 1.
38
+ call() {
39
+ local out
40
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
41
+ ' "$out" | tail -n 1)"; }
42
+ printf '%s
43
+ ' "$out" | tail -n 1
44
+ }
45
+
46
+ INPUT_JSON=""
47
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
48
+ INPUT_JSON="$1"
49
+ shift || true
50
+ fi
51
+ ID=""
52
+ while [[ $# -gt 0 ]]; do
53
+ case "$1" in
54
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
55
+ --help|-h) print_usage; exit 0 ;;
56
+ *) error_exit "Unknown option: $1" ;;
57
+ esac
58
+ done
59
+ if [ -n "$INPUT_JSON" ]; then
60
+ INPUT=$(read_json_input "$INPUT_JSON")
61
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .documentId // empty')
62
+ fi
63
+ # Accept a full docs.google.com URL too.
64
+ ID=$(printf '%s' "$ID" | sed -E 's#.*/document/d/([^/?]+).*#\1#')
65
+ require_param "id (--id)" "$ID"
66
+ RESPONSE=$(call GET "/google/docs/$(uri "$ID")") || { printf '%s\n' "$RESPONSE"; exit 1; }
67
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
68
+ printf '%s' "$RESPONSE" | jq -c '{id: .data.id, title: .data.title, webViewLink: .data.webViewLink, text: .data.text}'
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # Tests for docs-read — 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=18813
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': 'd1', 'title': 'T', 'webViewLink': 'https://l', 'text': '# T body'}}).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)
44
+ check "flags: output" "$OUT" '{"id":"d1","title":"T","webViewLink":"https://l","text":"# T body"}'
45
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/google/docs/d1"
46
+ run --id "https://docs.google.com/document/d/abc123/edit#x" >/dev/null
47
+ check "url: id extracted" "$(jq -r .path "$STUB_LOG")" "/api/google/docs/abc123"
48
+ run '{"documentId":"j1"}' >/dev/null
49
+ check "json: path" "$(jq -r .path "$STUB_LOG")" "/api/google/docs/j1"
50
+
51
+ echo "docs-read: $PASS passed, $FAIL failed"
52
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: Docs Write
3
+ description: Create a Google Doc in the owner's Drive, or append text to a document Crewly created (via the Google Workspace grant held by Crewly Cloud). Writes as the owner — say what you created and share the link.
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 google doc
18
+ - write a doc
19
+ - append to the doc
20
+ - put this in a google doc
21
+ tags:
22
+ - google
23
+ - docs
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Docs Write
33
+
34
+ ```bash
35
+ bash execute.sh --title "Weekly summary" --text-file /tmp/summary.md
36
+ bash execute.sh --id 1AbC… --text "Follow-ups: …"
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"success":true,"action":"create","id":"1AbC…","title":"Weekly summary","webViewLink":"https://docs.google.com/document/d/1AbC…/edit"}
43
+ ```
44
+
45
+ Text is inserted as plain paragraphs (Markdown is not rendered). Appending
46
+ to a document the owner made outside Crewly needs the Docs write scope —
47
+ a `403 google_error` means "create a new doc instead, or ask the owner to
48
+ re-connect Google Workspace once the scope is enabled".
49
+
50
+ ## Failures
51
+
52
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
53
+ when the owner has not connected Google Workspace (Settings → Integrations).
54
+ A `403` with `reason: "google_error"` on a write means the file was not
55
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # docs-write — Create a Google Doc, or append text to one Crewly created
4
+ #
5
+ # Backed by POST /api/google/docs (create) and POST /api/google/docs/:id/append.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --title "Meeting notes" --text "…" # create
9
+ # bash execute.sh --title "Meeting notes" --text-file notes.md
10
+ # bash execute.sh --id <documentId> --text "…" # append
11
+ # bash execute.sh '{"title":"Meeting notes","text":"…"}'
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 "Meeting notes" --text "…" # create
21
+ bash execute.sh --title "Meeting notes" --text-file notes.md
22
+ bash execute.sh --id <documentId> --text "…" # append
23
+ bash execute.sh '{"title":"Meeting notes","text":"…"}'
24
+
25
+ Options:
26
+ --title Create a new document with this title
27
+ --id Append to this document instead (must be one Crewly created, or the grant needs the Docs scope)
28
+ --text Body text (plain; blank line = new paragraph)
29
+ --text-file Read the body from a file
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=""; ID=""; TEXT=""; TEXT_FILE=""
59
+ while [[ $# -gt 0 ]]; do
60
+ case "$1" in
61
+ --title) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
62
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
63
+ --text) [ $# -ge 2 ] || error_exit "--text requires a value"; TEXT="$2"; shift 2 ;;
64
+ --text-file) [ $# -ge 2 ] || error_exit "--text-file requires a value"; TEXT_FILE="$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 "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .documentId // empty')
73
+ [ -z "$TEXT" ] && TEXT=$(printf '%s' "$INPUT" | jq -r '.text // .body // empty')
74
+ fi
75
+ if [ -n "$TEXT_FILE" ]; then
76
+ [ -f "$TEXT_FILE" ] || error_exit "text file not found: $TEXT_FILE"
77
+ TEXT=$(cat "$TEXT_FILE")
78
+ fi
79
+ ID=$(printf '%s' "$ID" | sed -E 's#.*/document/d/([^/?]+).*#\1#')
80
+ [ -n "$TITLE" ] || [ -n "$ID" ] || error_exit "either --title (create) or --id (append) is required"
81
+ if [ -n "$ID" ]; then
82
+ require_param "text (--text or --text-file)" "$TEXT"
83
+ BODY=$(jq -cn --arg text "$TEXT" '{text: $text}')
84
+ RESPONSE=$(call POST "/google/docs/$(uri "$ID")/append" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
85
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: "append", id: .data.id, title: .data.title, webViewLink: .data.webViewLink}'
86
+ else
87
+ BODY=$(jq -cn --arg title "$TITLE" --arg text "$TEXT" '{title: $title} + (if $text != "" then {text: $text} else {} end)')
88
+ RESPONSE=$(call POST "/google/docs" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
89
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: "create", id: .data.id, title: .data.title, webViewLink: .data.webViewLink}'
90
+ fi
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Tests for docs-write — 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=18814
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', 'T'), 'webViewLink': 'https://l'}}).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 "Notes" --text "hello")
44
+ check "create: output" "$OUT" '{"success":true,"action":"create","id":"d9","title":"Notes","webViewLink":"https://l"}'
45
+ check "create: path+body" "$(jq -r '.path + " " + (.body|tojson)' "$STUB_LOG")" '/api/google/docs {"title":"Notes","text":"hello"}'
46
+ TMP=$(mktemp); printf 'from file' > "$TMP"
47
+ run --id "https://docs.google.com/document/d/d1/edit" --text-file "$TMP" >/dev/null
48
+ check "append: path+body" "$(jq -r '.path + " " + (.body|tojson)' "$STUB_LOG")" '/api/google/docs/d1/append {"text":"from file"}'
49
+ rm -f "$TMP"
50
+ OUT=$(run_err --text "x" || true)
51
+ check "neither title nor id fails" "$(printf '%s' "$OUT" | grep -c 'required')" "1"
52
+
53
+ echo "docs-write: $PASS passed, $FAIL failed"
54
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Drive Read
3
+ description: Read the content of a Google Drive file (via the Google Workspace grant held by Crewly Cloud): Docs → text, Sheets → CSV, Slides → text, other files downloaded (text printed, binaries saved with --out). 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
+ - read drive file
18
+ - open file from drive
19
+ - download from drive
20
+ tags:
21
+ - google
22
+ - drive
23
+ execution:
24
+ type: script
25
+ script:
26
+ file: execute.sh
27
+ interpreter: bash
28
+ timeoutMs: 60000
29
+ ---
30
+
31
+ # Drive Read
32
+
33
+ ```bash
34
+ bash execute.sh --id 1AbC… # print content (text/CSV) as JSON
35
+ bash execute.sh --id 1AbC… --out /tmp/report.pdf # save (binary decoded) and print metadata
36
+ ```
37
+
38
+ ## Output
39
+
40
+ ```json
41
+ {"file":{"id":"1AbC…","name":"Q3 plan","mimeType":"application/vnd.google-apps.document","webViewLink":"https://…"},"contentType":"text/plain","encoding":"utf8","bytes":1234,"content":"…"}
42
+ ```
43
+
44
+ For a Google Doc / Sheet / Slides prefer `docs-read` / `sheets-read` /
45
+ `slides-read` — they keep structure (headings, cell grid, per-slide lines).
46
+ Files over 10 MB are refused with a `webViewLink` to open instead.
47
+
48
+ ## Failures
49
+
50
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
51
+ when the owner has not connected Google Workspace (Settings → Integrations).
52
+ A `403` with `reason: "google_error"` on a write means the file was not
53
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).