crewly 1.20.28 → 1.20.30

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 (114) 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/canva/canva-token.service.d.ts +98 -0
  64. package/dist/backend/backend/src/services/canva/canva-token.service.d.ts.map +1 -0
  65. package/dist/backend/backend/src/services/canva/canva-token.service.js +198 -0
  66. package/dist/backend/backend/src/services/canva/canva-token.service.js.map +1 -0
  67. package/dist/backend/backend/src/services/canva/canva.service.d.ts +184 -0
  68. package/dist/backend/backend/src/services/canva/canva.service.d.ts.map +1 -0
  69. package/dist/backend/backend/src/services/canva/canva.service.js +274 -0
  70. package/dist/backend/backend/src/services/canva/canva.service.js.map +1 -0
  71. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +2 -2
  72. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  73. package/dist/backend/backend/src/services/google/docs.service.d.ts +117 -0
  74. package/dist/backend/backend/src/services/google/docs.service.d.ts.map +1 -0
  75. package/dist/backend/backend/src/services/google/docs.service.js +152 -0
  76. package/dist/backend/backend/src/services/google/docs.service.js.map +1 -0
  77. package/dist/backend/backend/src/services/google/drive.service.d.ts +157 -0
  78. package/dist/backend/backend/src/services/google/drive.service.d.ts.map +1 -0
  79. package/dist/backend/backend/src/services/google/drive.service.js +203 -0
  80. package/dist/backend/backend/src/services/google/drive.service.js.map +1 -0
  81. package/dist/backend/backend/src/services/google/google-api.client.d.ts +15 -7
  82. package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/google/google-api.client.js +13 -6
  84. package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
  85. package/dist/backend/backend/src/services/google/sheets.service.d.ts +118 -0
  86. package/dist/backend/backend/src/services/google/sheets.service.d.ts.map +1 -0
  87. package/dist/backend/backend/src/services/google/sheets.service.js +158 -0
  88. package/dist/backend/backend/src/services/google/sheets.service.js.map +1 -0
  89. package/dist/backend/backend/src/services/google/slides.service.d.ts +136 -0
  90. package/dist/backend/backend/src/services/google/slides.service.d.ts.map +1 -0
  91. package/dist/backend/backend/src/services/google/slides.service.js +153 -0
  92. package/dist/backend/backend/src/services/google/slides.service.js.map +1 -0
  93. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +16 -3
  95. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  96. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  97. package/dist/backend/backend/src/services/slack/slack.service.js +11 -7
  98. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  99. package/dist/backend/build-info.json +2 -2
  100. package/dist/cli/backend/src/constants.d.ts +69 -0
  101. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  102. package/dist/cli/backend/src/constants.js +73 -0
  103. package/dist/cli/backend/src/constants.js.map +1 -1
  104. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +2 -2
  105. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  106. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  107. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +16 -3
  108. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  109. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  110. package/dist/cli/backend/src/services/slack/slack.service.js +11 -7
  111. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  112. package/frontend/dist/assets/{index-d1bd0dec.js → index-96b51cd9.js} +243 -243
  113. package/frontend/dist/index.html +1 -1
  114. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # drive-read — Read a Drive file's content (Docs/Sheets/Slides exported as text/CSV; other files downloaded)
4
+ #
5
+ # Backed by GET /api/google/drive/files/:id/content.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --id <fileId> [--out /path/to/save]
9
+ # bash execute.sh '{"id":"<fileId>"}'
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 <fileId> [--out /path/to/save]
19
+ bash execute.sh '{"id":"<fileId>"}'
20
+
21
+ Options:
22
+ --id Drive file id (required; from drive-search)
23
+ --out Save the content to this path instead of printing it (binary files are decoded)
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
+ ID=""; OUT_PATH=""
53
+ while [[ $# -gt 0 ]]; do
54
+ case "$1" in
55
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
56
+ --out|-o) [ $# -ge 2 ] || error_exit "--out requires a value"; OUT_PATH="$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 "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .fileId // empty')
64
+ [ -z "$OUT_PATH" ] && OUT_PATH=$(printf '%s' "$INPUT" | jq -r '.out // empty')
65
+ fi
66
+ require_param "id (--id)" "$ID"
67
+ RESPONSE=$(call GET "/google/drive/files/$(uri "$ID")/content") || { printf '%s\n' "$RESPONSE"; exit 1; }
68
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
69
+ if [ -n "$OUT_PATH" ]; then
70
+ ENC=$(printf '%s' "$RESPONSE" | jq -r '.data.encoding')
71
+ if [ "$ENC" = "base64" ]; then
72
+ printf '%s' "$RESPONSE" | jq -r '.data.content' | base64 -d > "$OUT_PATH"
73
+ else
74
+ printf '%s' "$RESPONSE" | jq -r '.data.content' > "$OUT_PATH"
75
+ fi
76
+ printf '%s' "$RESPONSE" | jq -c --arg p "$OUT_PATH" '{file: .data.file, contentType: .data.contentType, bytes: .data.bytes, savedTo: $p}'
77
+ exit 0
78
+ fi
79
+ printf '%s' "$RESPONSE" | jq -c '{file: .data.file, contentType: .data.contentType, encoding: .data.encoding, bytes: .data.bytes, content: .data.content}'
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # Tests for drive-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=18811
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': {'file': {'id': 'f1', 'name': 'a.txt', 'mimeType': 'text/plain'}, 'contentType': 'text/plain', 'encoding': 'utf8', 'bytes': 2, 'content': 'hi'}}).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 "f 1")
44
+ check "flags: output" "$OUT" '{"file":{"id":"f1","name":"a.txt","mimeType":"text/plain"},"contentType":"text/plain","encoding":"utf8","bytes":2,"content":"hi"}'
45
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/google/drive/files/f%201/content"
46
+ TMP=$(mktemp)
47
+ OUT=$(run '{"id":"f1"}' --out "$TMP")
48
+ check "out: saved" "$(cat "$TMP")" "hi"
49
+ check "out: metadata" "$OUT" "{\"file\":{\"id\":\"f1\",\"name\":\"a.txt\",\"mimeType\":\"text/plain\"},\"contentType\":\"text/plain\",\"bytes\":2,\"savedTo\":\"$TMP\"}"
50
+ rm -f "$TMP"
51
+ OUT=$(run_err || true)
52
+ check "missing id fails" "$(printf '%s' "$OUT" | grep -c 'id')" "1"
53
+
54
+ echo "drive-read: $PASS passed, $FAIL failed"
55
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: Drive Search
3
+ description: Search the owner's Google Drive by text, type or folder (via the Google Workspace grant held by Crewly Cloud). Returns file ids to pass to drive-read / docs-read / sheets-read / slides-read. 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
+ - search drive
18
+ - find file in drive
19
+ - look up a document
20
+ - which spreadsheet
21
+ tags:
22
+ - google
23
+ - drive
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Drive Search
33
+
34
+ ```bash
35
+ bash execute.sh --query "Q3 plan" # names + full text, newest first
36
+ bash execute.sh --mime sheet --max 5 # 5 most recent spreadsheets
37
+ bash execute.sh --folder 1AbC... --query budget # inside one folder
38
+ ```
39
+
40
+ ## Output
41
+
42
+ ```json
43
+ {"count":1,"files":[{"id":"1x…","name":"Q3 plan","mimeType":"application/vnd.google-apps.document","modifiedTime":"…","webViewLink":"https://…","owners":["ann@example.com"]}]}
44
+ ```
45
+
46
+ `mimeType` tells you which skill reads it: `…document` → `docs-read`,
47
+ `…spreadsheet` → `sheets-read`, `…presentation` → `slides-read`, anything
48
+ else → `drive-read`.
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,87 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # drive-search — Search the owner's Google Drive (read-only)
4
+ #
5
+ # Backed by GET /api/google/drive/files?q=&mimeType=&folderId=&max=.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --query "Q3 plan" [--mime doc|sheet|slides|pdf|folder|<mime>] [--folder <id>] [--max 20]
9
+ # bash execute.sh '{"query":"Q3 plan","mime":"sheet","max":20}'
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 --query "Q3 plan" [--mime doc|sheet|slides|pdf|folder|<mime>] [--folder <id>] [--max 20]
19
+ bash execute.sh '{"query":"Q3 plan","mime":"sheet","max":20}'
20
+
21
+ Options:
22
+ --query, -q Text matched against file names and full text (empty = most recent files)
23
+ --mime Restrict to a type: doc | sheet | slides | pdf | folder | any MIME type
24
+ --folder Restrict to a Drive folder id
25
+ --max Result cap (default 20, max 100)
26
+ --help | -h Show this help
27
+ EOF_USAGE
28
+ }
29
+
30
+ fail_from() {
31
+ printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
32
+ || jq -n --arg r "$1" '{success: false, reason: $r}'
33
+ exit 1
34
+ }
35
+
36
+ uri() { jq -rn --arg v "$1" '$v|@uri'; }
37
+
38
+ # api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
39
+ # the backend answer is always the last line. On failure print the mapped
40
+ # failure JSON (fail_from) and return 1.
41
+ call() {
42
+ local out
43
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
44
+ ' "$out" | tail -n 1)"; }
45
+ printf '%s
46
+ ' "$out" | tail -n 1
47
+ }
48
+
49
+ INPUT_JSON=""
50
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
51
+ INPUT_JSON="$1"
52
+ shift || true
53
+ fi
54
+ QUERY=""; MIME=""; FOLDER=""; MAX=""
55
+ while [[ $# -gt 0 ]]; do
56
+ case "$1" in
57
+ --query|-q) [ $# -ge 2 ] || error_exit "--query requires a value"; QUERY="$2"; shift 2 ;;
58
+ --mime) [ $# -ge 2 ] || error_exit "--mime requires a value"; MIME="$2"; shift 2 ;;
59
+ --folder) [ $# -ge 2 ] || error_exit "--folder requires a value"; FOLDER="$2"; shift 2 ;;
60
+ --max|-n) [ $# -ge 2 ] || error_exit "--max requires a value"; MAX="$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 "$QUERY" ] && QUERY=$(printf '%s' "$INPUT" | jq -r '.query // .q // empty')
68
+ [ -z "$MIME" ] && MIME=$(printf '%s' "$INPUT" | jq -r '.mime // .mimeType // empty')
69
+ [ -z "$FOLDER" ] && FOLDER=$(printf '%s' "$INPUT" | jq -r '.folder // .folderId // empty')
70
+ [ -z "$MAX" ] && MAX=$(printf '%s' "$INPUT" | jq -r '.max // empty')
71
+ fi
72
+ case "$MIME" in
73
+ doc|docs) MIME="application/vnd.google-apps.document" ;;
74
+ sheet|sheets) MIME="application/vnd.google-apps.spreadsheet" ;;
75
+ slides|slide|presentation) MIME="application/vnd.google-apps.presentation" ;;
76
+ pdf) MIME="application/pdf" ;;
77
+ folder) MIME="application/vnd.google-apps.folder" ;;
78
+ esac
79
+ QS=""
80
+ [ -n "$QUERY" ] && QS="${QS}&q=$(uri "$QUERY")"
81
+ [ -n "$MIME" ] && QS="${QS}&mimeType=$(uri "$MIME")"
82
+ [ -n "$FOLDER" ] && QS="${QS}&folderId=$(uri "$FOLDER")"
83
+ [ -n "$MAX" ] && QS="${QS}&max=$(uri "$MAX")"
84
+ QS="${QS#&}"
85
+ RESPONSE=$(call GET "/google/drive/files${QS:+?$QS}") || { printf '%s\n' "$RESPONSE"; exit 1; }
86
+ if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
87
+ printf '%s' "$RESPONSE" | jq -c '{count: .data.count, files: .data.files}'
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Tests for drive-search — 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=18810
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, 'files': [{'id': '1', 'name': 'Q3', 'mimeType': 'application/vnd.google-apps.document'}]}}).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 "Q3 plan" --mime sheet --folder f1 --max 5)
44
+ check "flags: output" "$OUT" '{"count":1,"files":[{"id":"1","name":"Q3","mimeType":"application/vnd.google-apps.document"}]}'
45
+ check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/google/drive/files?q=Q3%20plan&mimeType=application%2Fvnd.google-apps.spreadsheet&folderId=f1&max=5"
46
+ run '{"query":"x"}' >/dev/null
47
+ check "json: path" "$(jq -r .path "$STUB_LOG")" "/api/google/drive/files?q=x"
48
+ run >/dev/null
49
+ check "no args: bare list" "$(jq -r .path "$STUB_LOG")" "/api/google/drive/files"
50
+ OUT=$(run --query no-grant || true)
51
+ check "not connected" "$(printf '%s' "$OUT" | jq -r .reason)" "not_connected"
52
+
53
+ echo "drive-search: $PASS passed, $FAIL failed"
54
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: Drive Upload
3
+ description: Upload a local file or text to the owner's Google Drive (via the Google Workspace grant held by Crewly Cloud), optionally into a folder or converted to a Google Doc/Sheet/Slides. Files Crewly uploads can later be edited by docs-write / sheets-write.
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 drive
18
+ - save to google drive
19
+ - put file in drive
20
+ - share file via drive
21
+ tags:
22
+ - google
23
+ - drive
24
+ execution:
25
+ type: script
26
+ script:
27
+ file: execute.sh
28
+ interpreter: bash
29
+ timeoutMs: 60000
30
+ ---
31
+
32
+ # Drive Upload
33
+
34
+ ```bash
35
+ bash execute.sh --path ./report.pdf --folder 1Fo… # binary-safe
36
+ bash execute.sh --path ./notes.md --convert doc # becomes a Google Doc
37
+ bash execute.sh --name summary.txt --text "…" --folder 1Fo…
38
+ ```
39
+
40
+ ## Output
41
+
42
+ ```json
43
+ {"success":true,"file":{"id":"1AbC…","name":"report.pdf","mimeType":"application/pdf","webViewLink":"https://…"}}
44
+ ```
45
+
46
+ Limit: 10 MB per upload. Share the `webViewLink` with people (Drive
47
+ permissions are the owner's — this skill does not change sharing).
48
+
49
+ ## Failures
50
+
51
+ `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
52
+ when the owner has not connected Google Workspace (Settings → Integrations).
53
+ A `403` with `reason: "google_error"` on a write means the file was not
54
+ created by Crewly — the grant only edits files Crewly made (`drive.file`).
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # drive-upload — Upload a file (or text) to the owner's Google Drive
4
+ #
5
+ # Backed by POST /api/google/drive/files.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh --path /path/report.pdf [--name "Report.pdf"] [--folder <id>] [--convert doc|sheet|slides]
9
+ # bash execute.sh --name notes.txt --text "hello" [--mime text/plain]
10
+ # bash execute.sh '{"name":"notes.txt","text":"hello","folder":"<id>"}'
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 --path /path/report.pdf [--name "Report.pdf"] [--folder <id>] [--convert doc|sheet|slides]
20
+ bash execute.sh --name notes.txt --text "hello" [--mime text/plain]
21
+ bash execute.sh '{"name":"notes.txt","text":"hello","folder":"<id>"}'
22
+
23
+ Options:
24
+ --path, -p Local file to upload (binary-safe; MIME detected from the extension).
25
+ (Not --file: that flag is reserved by the skill runner for JSON input.)
26
+ --name Name in Drive (default: the file's basename)
27
+ --text Text content instead of --path
28
+ --mime MIME type override
29
+ --folder Destination folder id (default: My Drive root)
30
+ --convert Convert on upload: doc | sheet | slides (e.g. a .docx/.csv/.pptx becomes a Google-native file)
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
+ FILE=""; NAME=""; TEXT=""; MIME=""; FOLDER=""; CONVERT=""
60
+ while [[ $# -gt 0 ]]; do
61
+ case "$1" in
62
+ --path|-p) [ $# -ge 2 ] || error_exit "--path requires a value"; FILE="$2"; shift 2 ;;
63
+ --name) [ $# -ge 2 ] || error_exit "--name requires a value"; NAME="$2"; shift 2 ;;
64
+ --text) [ $# -ge 2 ] || error_exit "--text requires a value"; TEXT="$2"; shift 2 ;;
65
+ --mime) [ $# -ge 2 ] || error_exit "--mime requires a value"; MIME="$2"; shift 2 ;;
66
+ --folder) [ $# -ge 2 ] || error_exit "--folder requires a value"; FOLDER="$2"; shift 2 ;;
67
+ --convert) [ $# -ge 2 ] || error_exit "--convert requires a value"; CONVERT="$2"; shift 2 ;;
68
+ --help|-h) print_usage; exit 0 ;;
69
+ *) error_exit "Unknown option: $1" ;;
70
+ esac
71
+ done
72
+ if [ -n "$INPUT_JSON" ]; then
73
+ INPUT=$(read_json_input "$INPUT_JSON")
74
+ [ -z "$FILE" ] && FILE=$(printf '%s' "$INPUT" | jq -r '.path // .file // empty')
75
+ [ -z "$NAME" ] && NAME=$(printf '%s' "$INPUT" | jq -r '.name // empty')
76
+ [ -z "$TEXT" ] && TEXT=$(printf '%s' "$INPUT" | jq -r '.text // .content // empty')
77
+ [ -z "$MIME" ] && MIME=$(printf '%s' "$INPUT" | jq -r '.mime // .mimeType // empty')
78
+ [ -z "$FOLDER" ] && FOLDER=$(printf '%s' "$INPUT" | jq -r '.folder // .folderId // empty')
79
+ [ -z "$CONVERT" ] && CONVERT=$(printf '%s' "$INPUT" | jq -r '.convert // .convertTo // empty')
80
+ fi
81
+ case "$CONVERT" in
82
+ doc|docs) CONVERT="application/vnd.google-apps.document" ;;
83
+ sheet|sheets) CONVERT="application/vnd.google-apps.spreadsheet" ;;
84
+ slides|slide|presentation) CONVERT="application/vnd.google-apps.presentation" ;;
85
+ esac
86
+ mime_from_ext() {
87
+ case "${1##*.}" in
88
+ txt) echo text/plain ;; md) echo text/markdown ;; csv) echo text/csv ;; json) echo application/json ;;
89
+ html|htm) echo text/html ;; pdf) echo application/pdf ;; png) echo image/png ;; jpg|jpeg) echo image/jpeg ;;
90
+ gif) echo image/gif ;; mp4) echo video/mp4 ;; zip) echo application/zip ;;
91
+ docx) echo application/vnd.openxmlformats-officedocument.wordprocessingml.document ;;
92
+ xlsx) echo application/vnd.openxmlformats-officedocument.spreadsheetml.sheet ;;
93
+ pptx) echo application/vnd.openxmlformats-officedocument.presentationml.presentation ;;
94
+ *) echo application/octet-stream ;;
95
+ esac
96
+ }
97
+ ENCODING="utf8"; CONTENT=""
98
+ if [ -n "$FILE" ]; then
99
+ [ -f "$FILE" ] || error_exit "file not found: $FILE"
100
+ [ -z "$NAME" ] && NAME=$(basename "$FILE")
101
+ [ -z "$MIME" ] && MIME=$(mime_from_ext "$FILE")
102
+ case "$MIME" in
103
+ text/*|application/json) CONTENT=$(cat "$FILE") ;;
104
+ *) ENCODING="base64"; CONTENT=$(base64 < "$FILE" | tr -d '\n') ;;
105
+ esac
106
+ else
107
+ CONTENT="$TEXT"
108
+ [ -z "$MIME" ] && MIME="text/plain"
109
+ fi
110
+ require_param "name (--name or --path)" "$NAME"
111
+ require_param "content (--path or --text)" "$CONTENT"
112
+ BODY=$(jq -cn --arg name "$NAME" --arg content "$CONTENT" --arg enc "$ENCODING" --arg mime "$MIME" --arg folder "$FOLDER" --arg convert "$CONVERT" \
113
+ '{name: $name, content: $content, encoding: $enc, mimeType: $mime}
114
+ + (if $folder != "" then {folderId: $folder} else {} end)
115
+ + (if $convert != "" then {convertTo: $convert} else {} end)')
116
+ RESPONSE=$(call POST "/google/drive/files" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
117
+ printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), file: .data}'
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # Tests for drive-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=18812
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': 'u1', 'name': data.get('name'), 'mimeType': data.get('convertTo') or data.get('mimeType'), 'webViewLink': 'https://v'}}).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
+ TMP="$(mktemp).md"; printf 'hello' > "$TMP"
44
+ OUT=$(run --path "$TMP" --folder f1 --convert doc)
45
+ check "file: output" "$OUT" "{\"success\":true,\"file\":{\"id\":\"u1\",\"name\":\"$(basename "$TMP")\",\"mimeType\":\"application/vnd.google-apps.document\",\"webViewLink\":\"https://v\"}}"
46
+ check "file: body" "$(jq -c '.body' "$STUB_LOG")" "{\"name\":\"$(basename "$TMP")\",\"content\":\"hello\",\"encoding\":\"utf8\",\"mimeType\":\"text/markdown\",\"folderId\":\"f1\",\"convertTo\":\"application/vnd.google-apps.document\"}"
47
+ rm -f "$TMP"
48
+ BIN="$(mktemp).png"; printf '\x89PNG\x00' > "$BIN"
49
+ run --path "$BIN" >/dev/null
50
+ check "binary: base64" "$(jq -r '.body.encoding + " " + .body.content + " " + .body.mimeType' "$STUB_LOG")" "base64 iVBORwA= image/png"
51
+ rm -f "$BIN"
52
+ run --name a.txt --text "plain" >/dev/null
53
+ check "text: body" "$(jq -c '.body' "$STUB_LOG")" '{"name":"a.txt","content":"plain","encoding":"utf8","mimeType":"text/plain"}'
54
+ check "text: path" "$(jq -r .path "$STUB_LOG")" "/api/google/drive/files"
55
+
56
+ echo "drive-upload: $PASS passed, $FAIL failed"
57
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Sheets Read
3
+ description: Read a range of a Google Sheet as rows (unformatted values), or list its tabs with --info (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 spreadsheet
18
+ - read google sheet
19
+ - what is in the sheet
20
+ - pull rows from the sheet
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 Read
33
+
34
+ ```bash
35
+ bash execute.sh --id 1AbC… --info # {"id","title","sheets":[{"title":"Q3",...}]}
36
+ bash execute.sh --id 1AbC… --range "Q3!A1:D50"
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"spreadsheetId":"1AbC…","range":"Q3!A1:D50","rowCount":2,"rows":[["name","email"],["Ann","ann@example.com"]]}
43
+ ```
44
+
45
+ Rows may be ragged (trailing empty cells are omitted by Google). Numbers
46
+ come back as numbers, dates as the sheet's formatted string.
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`).