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.
- package/config/skills/agent/core/canva-create/SKILL.md +53 -0
- package/config/skills/agent/core/canva-create/execute.sh +88 -0
- package/config/skills/agent/core/canva-create/execute.test.sh +54 -0
- package/config/skills/agent/core/canva-designs/SKILL.md +51 -0
- package/config/skills/agent/core/canva-designs/execute.sh +96 -0
- package/config/skills/agent/core/canva-designs/execute.test.sh +54 -0
- package/config/skills/agent/core/canva-export/SKILL.md +52 -0
- package/config/skills/agent/core/canva-export/execute.sh +111 -0
- package/config/skills/agent/core/canva-export/execute.test.sh +55 -0
- package/config/skills/agent/core/canva-upload/SKILL.md +49 -0
- package/config/skills/agent/core/canva-upload/execute.sh +72 -0
- package/config/skills/agent/core/canva-upload/execute.test.sh +54 -0
- package/config/skills/agent/core/docs-read/SKILL.md +50 -0
- package/config/skills/agent/core/docs-read/execute.sh +68 -0
- package/config/skills/agent/core/docs-read/execute.test.sh +52 -0
- package/config/skills/agent/core/docs-write/SKILL.md +55 -0
- package/config/skills/agent/core/docs-write/execute.sh +90 -0
- package/config/skills/agent/core/docs-write/execute.test.sh +54 -0
- package/config/skills/agent/core/drive-read/SKILL.md +53 -0
- package/config/skills/agent/core/drive-read/execute.sh +79 -0
- package/config/skills/agent/core/drive-read/execute.test.sh +55 -0
- package/config/skills/agent/core/drive-search/SKILL.md +55 -0
- package/config/skills/agent/core/drive-search/execute.sh +87 -0
- package/config/skills/agent/core/drive-search/execute.test.sh +54 -0
- package/config/skills/agent/core/drive-upload/SKILL.md +54 -0
- package/config/skills/agent/core/drive-upload/execute.sh +117 -0
- package/config/skills/agent/core/drive-upload/execute.test.sh +57 -0
- package/config/skills/agent/core/sheets-read/SKILL.md +53 -0
- package/config/skills/agent/core/sheets-read/execute.sh +81 -0
- package/config/skills/agent/core/sheets-read/execute.test.sh +52 -0
- package/config/skills/agent/core/sheets-write/SKILL.md +56 -0
- package/config/skills/agent/core/sheets-write/execute.sh +103 -0
- package/config/skills/agent/core/sheets-write/execute.test.sh +57 -0
- package/config/skills/agent/core/slides-create/SKILL.md +61 -0
- package/config/skills/agent/core/slides-create/execute.sh +92 -0
- package/config/skills/agent/core/slides-create/execute.test.sh +54 -0
- package/config/skills/agent/core/slides-read/SKILL.md +49 -0
- package/config/skills/agent/core/slides-read/execute.sh +67 -0
- package/config/skills/agent/core/slides-read/execute.test.sh +50 -0
- package/dist/backend/backend/src/constants.d.ts +69 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +73 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts +48 -0
- package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/canva/canva.controller.js +183 -0
- package/dist/backend/backend/src/controllers/canva/canva.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.d.ts +22 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.js +34 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts +100 -0
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.controller.js +237 -1
- package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts +13 -0
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js +27 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +3 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +22 -4
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/canva/canva-token.service.d.ts +98 -0
- package/dist/backend/backend/src/services/canva/canva-token.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/canva/canva-token.service.js +198 -0
- package/dist/backend/backend/src/services/canva/canva-token.service.js.map +1 -0
- package/dist/backend/backend/src/services/canva/canva.service.d.ts +184 -0
- package/dist/backend/backend/src/services/canva/canva.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/canva/canva.service.js +274 -0
- package/dist/backend/backend/src/services/canva/canva.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/docs.service.d.ts +117 -0
- package/dist/backend/backend/src/services/google/docs.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/docs.service.js +152 -0
- package/dist/backend/backend/src/services/google/docs.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/drive.service.d.ts +157 -0
- package/dist/backend/backend/src/services/google/drive.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/drive.service.js +203 -0
- package/dist/backend/backend/src/services/google/drive.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/google-api.client.d.ts +15 -7
- package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
- package/dist/backend/backend/src/services/google/google-api.client.js +13 -6
- package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
- package/dist/backend/backend/src/services/google/sheets.service.d.ts +118 -0
- package/dist/backend/backend/src/services/google/sheets.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/sheets.service.js +158 -0
- package/dist/backend/backend/src/services/google/sheets.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/slides.service.d.ts +136 -0
- package/dist/backend/backend/src/services/google/slides.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/slides.service.js +153 -0
- package/dist/backend/backend/src/services/google/slides.service.js.map +1 -0
- package/dist/backend/backend/src/utils/tool-call-markup.utils.d.ts +45 -0
- package/dist/backend/backend/src/utils/tool-call-markup.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/tool-call-markup.utils.js +83 -0
- package/dist/backend/backend/src/utils/tool-call-markup.utils.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +69 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +73 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +22 -4
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/utils/tool-call-markup.utils.d.ts +45 -0
- package/dist/cli/backend/src/utils/tool-call-markup.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/tool-call-markup.utils.js +83 -0
- package/dist/cli/backend/src/utils/tool-call-markup.utils.js.map +1 -0
- package/frontend/dist/assets/{index-d1bd0dec.js → index-96b51cd9.js} +243 -243
- package/frontend/dist/index.html +1 -1
- 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`).
|