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.
- 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/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/chat-v2/chat-v2.dispatcher.service.js +2 -2
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- 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/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +16 -3
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +11 -7
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- 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/chat-v2/chat-v2.dispatcher.service.js +2 -2
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +16 -3
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +11 -7
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- 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,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`).
|