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,81 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # sheets-read — Read cells (or the tab list) of a Google Sheet
4
+ #
5
+ # Backed by GET /api/google/sheets/:id/values?range= and GET /api/google/sheets/:id.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --id <spreadsheetId> [--range "Sheet1!A1:D50"]
9
+ # bash execute.sh --id <spreadsheetId> --info # title + tabs only
10
+ # bash execute.sh '{"id":"<spreadsheetId>","range":"A1:D50"}'
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 --id <spreadsheetId> [--range "Sheet1!A1:D50"]
20
+ bash execute.sh --id <spreadsheetId> --info # title + tabs only
21
+ bash execute.sh '{"id":"<spreadsheetId>","range":"A1:D50"}'
22
+
23
+ Options:
24
+ --id Spreadsheet id (or its URL)
25
+ --range A1 range, e.g. "Q3!A1:D50" (default A1:Z1000 on the first tab)
26
+ --info Print the title and tab names instead of values
27
+ --help | -h Show this help
28
+ EOF_USAGE
29
+ }
30
+
31
+ fail_from() {
32
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
33
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
34
+ exit 1
35
+ }
36
+
37
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
38
+
39
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
40
+ # the backend answer is always the last line. On failure print the mapped
41
+ # failure JSON (fail_from) and return 1.
42
+ call() {
43
+ local out
44
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
45
+ ' "$out" | tail -n 1)"; }
46
+ printf '%s
47
+ ' "$out" | tail -n 1
48
+ }
49
+
50
+ INPUT_JSON=""
51
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
52
+ INPUT_JSON="$1"
53
+ shift || true
54
+ fi
55
+ ID=""; RANGE=""; INFO=""
56
+ while [[ $# -gt 0 ]]; do
57
+ case "$1" in
58
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
59
+ --range) [ $# -ge 2 ] || error_exit "--range requires a value"; RANGE="$2"; shift 2 ;;
60
+ --info) INFO=1; shift ;;
61
+ --help|-h) print_usage; exit 0 ;;
62
+ *) error_exit "Unknown option: $1" ;;
63
+ esac
64
+ done
65
+ if [ -n "$INPUT_JSON" ]; then
66
+ INPUT=$(read_json_input "$INPUT_JSON")
67
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .spreadsheetId // empty')
68
+ [ -z "$RANGE" ] && RANGE=$(printf '%s' "$INPUT" | jq -r '.range // empty')
69
+ [ -z "$INFO" ] && INFO=$(printf '%s' "$INPUT" | jq -r 'if .info == true then "1" else "" end')
70
+ fi
71
+ ID=$(printf '%s' "$ID" | sed -E 's#.*/spreadsheets/d/([^/?]+).*#\1#')
72
+ require_param "id (--id)" "$ID"
73
+ if [ -n "$INFO" ]; then
74
+ RESPONSE=$(call GET "/google/sheets/$(uri "$ID")") || { printf '%s\n' "$RESPONSE"; exit 1; }
75
+ printf '%s' "$RESPONSE" | jq -c '.data'
76
+ exit 0
77
+ fi
78
+ QS=""; [ -n "$RANGE" ] && QS="?range=$(uri "$RANGE")"
79
+ RESPONSE=$(call GET "/google/sheets/$(uri "$ID")/values${QS}") || { printf '%s\n' "$RESPONSE"; exit 1; }
80
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
81
+ printf '%s' "$RESPONSE" | jq -c '{spreadsheetId: .data.spreadsheetId, range: .data.range, rowCount: (.data.rows | length), rows: .data.rows}'
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # Tests for sheets-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=18815
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': {'spreadsheetId': 's1', 'range': 'Q3!A1:B2', 'rows': [['a', 1]], 'id': 's1', 'title': 'Budget', 'sheets': [{'title': 'Q3'}]}}).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 s1 --range "Q3!A1:B2")
44
+ check "values: output" "$OUT" '{"spreadsheetId":"s1","range":"Q3!A1:B2","rowCount":1,"rows":[["a",1]]}'
45
+ check "values: path" "$(jq -r .path "$STUB_LOG")" "/api/google/sheets/s1/values?range=Q3%21A1%3AB2"
46
+ run --id "https://docs.google.com/spreadsheets/d/xyz/edit#gid=0" >/dev/null
47
+ check "url: default range" "$(jq -r .path "$STUB_LOG")" "/api/google/sheets/xyz/values"
48
+ run '{"id":"s1","info":true}' >/dev/null
49
+ check "info: path" "$(jq -r .path "$STUB_LOG")" "/api/google/sheets/s1"
50
+
51
+ echo "sheets-read: $PASS passed, $FAIL failed"
52
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: Sheets Write
3
+ description: Create a Google Sheet with rows, or append / overwrite rows in a spreadsheet Crewly created (via the Google Workspace grant held by Crewly Cloud). Takes JSON rows or a CSV file. Writes as the owner — share the link when done.
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 spreadsheet
18
+ - write to google sheet
19
+ - add rows to the sheet
20
+ - export to sheets
21
+ tags:
22
+ - google
23
+ - sheets
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Sheets Write
33
+
34
+ ```bash
35
+ bash execute.sh --title "Leads Sept" --csv-file /tmp/leads.csv --sheet "Raw"
36
+ bash execute.sh --id 1AbC… --rows '[["Bob","bob@x"]]' # append below the table at A1
37
+ bash execute.sh --id 1AbC… --rows '[["done"]]' --range "Raw!C2" --mode update
38
+ ```
39
+
40
+ ## Output
41
+
42
+ ```json
43
+ {"success":true,"action":"create","id":"1AbC…","title":"Leads Sept","sheets":["Raw"],"webViewLink":"https://docs.google.com/spreadsheets/d/1AbC…/edit"}
44
+ {"success":true,"action":"append","spreadsheetId":"1AbC…","updatedRange":"Raw!A3:B3","updatedRows":1}
45
+ ```
46
+
47
+ Values are entered as if typed (`USER_ENTERED`): `=SUM(A1:A9)` becomes a
48
+ formula, `2026-09-19` a date. Max 5000 rows per call. Writing to a sheet
49
+ the owner made outside Crewly needs the Sheets write scope (`403 google_error`).
50
+
51
+ ## Failures
52
+
53
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
54
+ when the owner has not connected Google Workspace (Settings → Integrations).
55
+ A `403` with `reason: "google_error"` on a write means the file was not
56
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).
@@ -0,0 +1,103 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # sheets-write — Create a Google Sheet, or append/overwrite rows in one Crewly created
4
+ #
5
+ # Backed by POST /api/google/sheets (create) and POST /api/google/sheets/:id/values.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --title "Leads" --rows '[["name","email"],["Ann","a@x"]]' [--sheet "Raw"]
9
+ # bash execute.sh --title "Leads" --csv-file leads.csv
10
+ # bash execute.sh --id <spreadsheetId> --rows '[["Bob","b@x"]]' [--range "Raw!A1"] # append
11
+ # bash execute.sh --id <spreadsheetId> --rows '[["v"]]' --range "Raw!B2" --mode update # overwrite
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 "Leads" --rows '[["name","email"],["Ann","a@x"]]' [--sheet "Raw"]
21
+ bash execute.sh --title "Leads" --csv-file leads.csv
22
+ bash execute.sh --id <spreadsheetId> --rows '[["Bob","b@x"]]' [--range "Raw!A1"] # append
23
+ bash execute.sh --id <spreadsheetId> --rows '[["v"]]' --range "Raw!B2" --mode update # overwrite
24
+
25
+ Options:
26
+ --title Create a new spreadsheet with this title
27
+ --sheet Name of the first tab when creating (default Sheet1)
28
+ --id Write into this spreadsheet instead (must be one Crewly created, or the grant needs the Sheets scope)
29
+ --rows JSON array of arrays (strings / numbers / booleans)
30
+ --csv-file Rows from a CSV file (simple: comma-separated, quotes stripped)
31
+ --range A1 anchor: the table to append to, or the top-left cell to overwrite (default A1)
32
+ --mode append (default) | update
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
+ TITLE=""; SHEET=""; ID=""; ROWS=""; CSV_FILE=""; RANGE=""; MODE=""
62
+ while [[ $# -gt 0 ]]; do
63
+ case "$1" in
64
+ --title) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
65
+ --sheet) [ $# -ge 2 ] || error_exit "--sheet requires a value"; SHEET="$2"; shift 2 ;;
66
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
67
+ --rows) [ $# -ge 2 ] || error_exit "--rows requires a value"; ROWS="$2"; shift 2 ;;
68
+ --csv-file) [ $# -ge 2 ] || error_exit "--csv-file requires a value"; CSV_FILE="$2"; shift 2 ;;
69
+ --range) [ $# -ge 2 ] || error_exit "--range requires a value"; RANGE="$2"; shift 2 ;;
70
+ --mode) [ $# -ge 2 ] || error_exit "--mode requires a value"; MODE="$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 "$TITLE" ] && TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
78
+ [ -z "$SHEET" ] && SHEET=$(printf '%s' "$INPUT" | jq -r '.sheet // .sheetTitle // empty')
79
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .spreadsheetId // empty')
80
+ [ -z "$ROWS" ] && ROWS=$(printf '%s' "$INPUT" | jq -c '.rows // empty')
81
+ [ -z "$RANGE" ] && RANGE=$(printf '%s' "$INPUT" | jq -r '.range // empty')
82
+ [ -z "$MODE" ] && MODE=$(printf '%s' "$INPUT" | jq -r '.mode // empty')
83
+ fi
84
+ if [ -n "$CSV_FILE" ]; then
85
+ [ -f "$CSV_FILE" ] || error_exit "csv file not found: $CSV_FILE"
86
+ ROWS=$(python3 -c 'import csv,json,sys; print(json.dumps(list(csv.reader(open(sys.argv[1], newline="")))))' "$CSV_FILE")
87
+ fi
88
+ ID=$(printf '%s' "$ID" | sed -E 's#.*/spreadsheets/d/([^/?]+).*#\1#')
89
+ [ -n "$TITLE" ] || [ -n "$ID" ] || error_exit "either --title (create) or --id (write) is required"
90
+ [ -z "$ROWS" ] || printf '%s' "$ROWS" | jq -e 'type == "array" and all(.[]; type == "array")' >/dev/null 2>&1 || error_exit "--rows must be a JSON array of arrays"
91
+ case "$MODE" in ""|append|update) ;; *) error_exit "--mode must be append or update" ;; esac
92
+ if [ -n "$ID" ]; then
93
+ require_param "rows (--rows or --csv-file)" "$ROWS"
94
+ BODY=$(jq -cn --argjson rows "$ROWS" --arg range "$RANGE" --arg mode "${MODE:-append}" \
95
+ '{rows: $rows, mode: $mode} + (if $range != "" then {range: $range} else {} end)')
96
+ RESPONSE=$(call POST "/google/sheets/$(uri "$ID")/values" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
97
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: .data.mode, spreadsheetId: .data.spreadsheetId, updatedRange: .data.updatedRange, updatedRows: .data.updatedRows}'
98
+ else
99
+ BODY=$(jq -cn --arg title "$TITLE" --arg sheet "$SHEET" --argjson rows "${ROWS:-[]}" \
100
+ '{title: $title} + (if $sheet != "" then {sheetTitle: $sheet} else {} end) + (if ($rows|length) > 0 then {rows: $rows} else {} end)')
101
+ RESPONSE=$(call POST "/google/sheets" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
102
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: "create", id: .data.id, title: .data.title, sheets: [.data.sheets[]?.title], webViewLink: .data.webViewLink}'
103
+ fi
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # Tests for sheets-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=18816
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': 'n1', 'title': data.get('title', 'T'), 'sheets': [{'title': 'Raw'}], 'webViewLink': 'https://l', 'mode': data.get('mode'), 'spreadsheetId': 's1', 'updatedRange': 'Raw!A3:B3', 'updatedRows': len(data.get('rows', []))}}).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 "Leads" --sheet Raw --rows '[["name","email"],["Ann","a@x"]]')
44
+ check "create: output" "$OUT" '{"success":true,"action":"create","id":"n1","title":"Leads","sheets":["Raw"],"webViewLink":"https://l"}'
45
+ check "create: body" "$(jq -c .body "$STUB_LOG")" '{"title":"Leads","sheetTitle":"Raw","rows":[["name","email"],["Ann","a@x"]]}'
46
+ TMP="$(mktemp).csv"; printf 'name,email\n"Bob, Jr",b@x\n' > "$TMP"
47
+ OUT=$(run --id "https://docs.google.com/spreadsheets/d/s1/edit" --csv-file "$TMP")
48
+ check "append: output" "$OUT" '{"success":true,"action":"append","spreadsheetId":"s1","updatedRange":"Raw!A3:B3","updatedRows":2}'
49
+ check "append: body" "$(jq -c .body "$STUB_LOG")" '{"rows":[["name","email"],["Bob, Jr","b@x"]],"mode":"append"}'
50
+ rm -f "$TMP"
51
+ run --id s1 --rows '[["v"]]' --range "Raw!B2" --mode update >/dev/null
52
+ check "update: path+body" "$(jq -r '.path + " " + (.body|tojson)' "$STUB_LOG")" '/api/google/sheets/s1/values {"rows":[["v"]],"mode":"update","range":"Raw!B2"}'
53
+ OUT=$(run_err --id s1 --rows '["not","nested"]' || true)
54
+ check "bad rows rejected" "$(printf '%s' "$OUT" | grep -c 'array of arrays')" "1"
55
+
56
+ echo "sheets-write: $PASS passed, $FAIL failed"
57
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: Slides Create
3
+ description: Create a Google Slides deck in the owner's Drive from an outline — one TITLE_AND_BODY slide per entry with its title and bullet lines (via the Google Workspace grant held by Crewly Cloud). Share the link when done.
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 slides
18
+ - make a deck
19
+ - build a presentation
20
+ - turn this into slides
21
+ tags:
22
+ - google
23
+ - slides
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Slides Create
33
+
34
+ ```bash
35
+ bash execute.sh --title "Q3 review" --outline-file /tmp/q3.md
36
+ ```
37
+
38
+ `/tmp/q3.md`:
39
+ ```
40
+ # Highlights
41
+ - Revenue +12%
42
+ - Two launches
43
+ # Risks
44
+ - Hiring
45
+ ```
46
+
47
+ ## Output
48
+
49
+ ```json
50
+ {"success":true,"id":"1AbC…","title":"Q3 review","slideCount":2,"webViewLink":"https://docs.google.com/presentation/d/1AbC…/edit"}
51
+ ```
52
+
53
+ Layout is the default theme's TITLE_AND_BODY; no images. Max 60 slides.
54
+ For designed decks (brand templates, images, video) use the Canva skills.
55
+
56
+ ## Failures
57
+
58
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
59
+ when the owner has not connected Google Workspace (Settings → Integrations).
60
+ A `403` with `reason: "google_error"` on a write means the file was not
61
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # slides-create — Create a Google Slides deck from an outline (title + bullets per slide)
4
+ #
5
+ # Backed by POST /api/google/slides.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --title "Pitch" --slides '[{"title":"Why now","bullets":["Market","Timing"]},{"title":"Plan"}]'
9
+ # bash execute.sh --title "Pitch" --outline-file deck.md # "# Slide title" then "- bullet" lines
10
+ # bash execute.sh '{"title":"Pitch","slides":[{"title":"Why now","bullets":["Market"]}]}'
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 --title "Pitch" --slides '[{"title":"Why now","bullets":["Market","Timing"]},{"title":"Plan"}]'
20
+ bash execute.sh --title "Pitch" --outline-file deck.md # "# Slide title" then "- bullet" lines
21
+ bash execute.sh '{"title":"Pitch","slides":[{"title":"Why now","bullets":["Market"]}]}'
22
+
23
+ Options:
24
+ --title Deck title (required)
25
+ --slides JSON array of {title, bullets[]}
26
+ --outline-file Markdown outline: each "# Heading" starts a slide, "-"/"*" lines are its bullets
27
+ --help | -h Show this help
28
+ EOF_USAGE
29
+ }
30
+
31
+ fail_from() {
32
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
33
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
34
+ exit 1
35
+ }
36
+
37
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
38
+
39
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
40
+ # the backend answer is always the last line. On failure print the mapped
41
+ # failure JSON (fail_from) and return 1.
42
+ call() {
43
+ local out
44
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
45
+ ' "$out" | tail -n 1)"; }
46
+ printf '%s
47
+ ' "$out" | tail -n 1
48
+ }
49
+
50
+ INPUT_JSON=""
51
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
52
+ INPUT_JSON="$1"
53
+ shift || true
54
+ fi
55
+ TITLE=""; SLIDES=""; OUTLINE_FILE=""
56
+ while [[ $# -gt 0 ]]; do
57
+ case "$1" in
58
+ --title) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
59
+ --slides) [ $# -ge 2 ] || error_exit "--slides requires a value"; SLIDES="$2"; shift 2 ;;
60
+ --outline-file) [ $# -ge 2 ] || error_exit "--outline-file requires a value"; OUTLINE_FILE="$2"; shift 2 ;;
61
+ --help|-h) print_usage; exit 0 ;;
62
+ *) error_exit "Unknown option: $1" ;;
63
+ esac
64
+ done
65
+ if [ -n "$INPUT_JSON" ]; then
66
+ INPUT=$(read_json_input "$INPUT_JSON")
67
+ [ -z "$TITLE" ] && TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
68
+ [ -z "$SLIDES" ] && SLIDES=$(printf '%s' "$INPUT" | jq -c '.slides // empty')
69
+ fi
70
+ if [ -n "$OUTLINE_FILE" ]; then
71
+ [ -f "$OUTLINE_FILE" ] || error_exit "outline file not found: $OUTLINE_FILE"
72
+ SLIDES=$(python3 - "$OUTLINE_FILE" <<'PY'
73
+ import json, re, sys
74
+ slides = []
75
+ for raw in open(sys.argv[1], encoding='utf-8'):
76
+ line = raw.rstrip('\n')
77
+ if re.match(r'^#{1,6}\s+', line):
78
+ slides.append({'title': re.sub(r'^#{1,6}\s+', '', line).strip(), 'bullets': []})
79
+ elif re.match(r'^\s*[-*]\s+', line) and slides:
80
+ slides[-1]['bullets'].append(re.sub(r'^\s*[-*]\s+', '', line).strip())
81
+ elif line.strip() and slides:
82
+ slides[-1]['bullets'].append(line.strip())
83
+ print(json.dumps(slides, ensure_ascii=False))
84
+ PY
85
+ )
86
+ fi
87
+ require_param "title (--title)" "$TITLE"
88
+ require_param "slides (--slides or --outline-file)" "$SLIDES"
89
+ printf '%s' "$SLIDES" | jq -e 'type == "array" and length > 0 and all(.[]; type == "object")' >/dev/null 2>&1 || error_exit "--slides must be a non-empty JSON array of {title, bullets[]}"
90
+ BODY=$(jq -cn --arg title "$TITLE" --argjson slides "$SLIDES" '{title: $title, slides: $slides}')
91
+ RESPONSE=$(call POST "/google/slides" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
92
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), id: .data.id, title: .data.title, slideCount: .data.slideCount, webViewLink: .data.webViewLink}'
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Tests for slides-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=18818
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': 'p9', 'title': data.get('title'), 'slideCount': len(data.get('slides', [])), '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 "Pitch" --slides '[{"title":"Why","bullets":["a"]},{"title":"Plan"}]')
44
+ check "json slides: output" "$OUT" '{"success":true,"id":"p9","title":"Pitch","slideCount":2,"webViewLink":"https://l"}'
45
+ check "json slides: body" "$(jq -c .body "$STUB_LOG")" '{"title":"Pitch","slides":[{"title":"Why","bullets":["a"]},{"title":"Plan"}]}'
46
+ TMP="$(mktemp).md"; printf '# Highlights\n- Revenue +12%%\n* Two launches\n\n## Risks\nHiring is slow\n' > "$TMP"
47
+ run --title "Q3" --outline-file "$TMP" >/dev/null
48
+ check "outline: parsed" "$(jq -c .body.slides "$STUB_LOG")" '[{"title":"Highlights","bullets":["Revenue +12%","Two launches"]},{"title":"Risks","bullets":["Hiring is slow"]}]'
49
+ rm -f "$TMP"
50
+ OUT=$(run_err --title "x" --slides '[]' || true)
51
+ check "empty slides rejected" "$(printf '%s' "$OUT" | grep -c 'non-empty')" "1"
52
+
53
+ echo "slides-create: $PASS passed, $FAIL failed"
54
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: Slides Read
3
+ description: Read a Google Slides presentation as text — every text line per slide plus speaker notes (via the Google Workspace grant held by Crewly Cloud). Accepts an id or 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 the slides
18
+ - open the presentation
19
+ - what is in the deck
20
+ - summarize the deck
21
+ tags:
22
+ - google
23
+ - slides
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Slides Read
33
+
34
+ ```bash
35
+ bash execute.sh --id 1AbC…
36
+ ```
37
+
38
+ ## Output
39
+
40
+ ```json
41
+ {"id":"1AbC…","title":"Pitch","slideCount":2,"webViewLink":"https://…","slides":[{"index":1,"lines":["Why now","Market is moving"],"notes":"say hi"},{"index":2,"lines":["Plan"]}]}
42
+ ```
43
+
44
+ ## Failures
45
+
46
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
47
+ when the owner has not connected Google Workspace (Settings → Integrations).
48
+ A `403` with `reason: "google_error"` on a write means the file was not
49
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).