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