crewly 1.20.35 → 1.20.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/config/skills/_common/desktop-guards.sh +485 -0
  2. package/config/skills/_common/desktop-guards.test.sh +242 -0
  3. package/config/skills/_common/desktop-perceive.swift +530 -0
  4. package/config/skills/_common/desktop-presence.swift +343 -0
  5. package/config/skills/_common/lib.sh +6 -0
  6. package/config/skills/agent/_common/desktop-guards.sh +4 -0
  7. package/config/skills/agent/computer-use/SKILL.md +88 -0
  8. package/config/skills/agent/computer-use/execute.sh +249 -3
  9. package/config/skills/agent/core/calendar-create/SKILL.md +10 -0
  10. package/config/skills/agent/core/calendar-create/execute.sh +6 -0
  11. package/config/skills/agent/core/calendar-list/SKILL.md +10 -0
  12. package/config/skills/agent/core/calendar-list/execute.sh +6 -0
  13. package/config/skills/agent/core/docs-read/SKILL.md +10 -0
  14. package/config/skills/agent/core/docs-read/execute.sh +6 -1
  15. package/config/skills/agent/core/docs-write/SKILL.md +10 -0
  16. package/config/skills/agent/core/docs-write/execute.sh +6 -1
  17. package/config/skills/agent/core/drive-read/SKILL.md +10 -0
  18. package/config/skills/agent/core/drive-read/execute.sh +6 -1
  19. package/config/skills/agent/core/drive-search/SKILL.md +10 -0
  20. package/config/skills/agent/core/drive-search/execute.sh +6 -1
  21. package/config/skills/agent/core/drive-upload/SKILL.md +10 -0
  22. package/config/skills/agent/core/drive-upload/execute.sh +6 -1
  23. package/config/skills/agent/core/gmail-read/SKILL.md +10 -0
  24. package/config/skills/agent/core/gmail-read/execute.sh +6 -0
  25. package/config/skills/agent/core/gmail-search/SKILL.md +10 -0
  26. package/config/skills/agent/core/gmail-search/execute.sh +6 -0
  27. package/config/skills/agent/core/gmail-send/SKILL.md +10 -0
  28. package/config/skills/agent/core/gmail-send/execute.sh +6 -0
  29. package/config/skills/agent/core/sheets-read/SKILL.md +10 -0
  30. package/config/skills/agent/core/sheets-read/execute.sh +6 -1
  31. package/config/skills/agent/core/sheets-write/SKILL.md +10 -0
  32. package/config/skills/agent/core/sheets-write/execute.sh +6 -1
  33. package/config/skills/agent/core/slides-create/SKILL.md +10 -0
  34. package/config/skills/agent/core/slides-create/execute.sh +6 -1
  35. package/config/skills/agent/core/slides-read/SKILL.md +10 -0
  36. package/config/skills/agent/core/slides-read/execute.sh +6 -1
  37. package/config/skills/agent/desktop-app-control/SKILL.md +19 -0
  38. package/config/skills/agent/remote-browser/SKILL.md +19 -0
  39. package/config/slack-app-manifest.json +16 -9
  40. package/dist/backend/backend/src/constants.d.ts +18 -4
  41. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  42. package/dist/backend/backend/src/constants.js +16 -4
  43. package/dist/backend/backend/src/constants.js.map +1 -1
  44. package/dist/backend/backend/src/controllers/desktop/desktop.controller.d.ts +105 -0
  45. package/dist/backend/backend/src/controllers/desktop/desktop.controller.d.ts.map +1 -0
  46. package/dist/backend/backend/src/controllers/desktop/desktop.controller.js +278 -0
  47. package/dist/backend/backend/src/controllers/desktop/desktop.controller.js.map +1 -0
  48. package/dist/backend/backend/src/controllers/desktop/desktop.routes.d.ts +21 -0
  49. package/dist/backend/backend/src/controllers/desktop/desktop.routes.d.ts.map +1 -0
  50. package/dist/backend/backend/src/controllers/desktop/desktop.routes.js +31 -0
  51. package/dist/backend/backend/src/controllers/desktop/desktop.routes.js.map +1 -0
  52. package/dist/backend/backend/src/controllers/google/google.controller.d.ts +8 -0
  53. package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
  54. package/dist/backend/backend/src/controllers/google/google.controller.js +137 -37
  55. package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
  56. package/dist/backend/backend/src/controllers/google/google.routes.d.ts +2 -1
  57. package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
  58. package/dist/backend/backend/src/controllers/google/google.routes.js +4 -2
  59. package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
  60. package/dist/backend/backend/src/controllers/slack/slack-error.utils.d.ts +46 -0
  61. package/dist/backend/backend/src/controllers/slack/slack-error.utils.d.ts.map +1 -0
  62. package/dist/backend/backend/src/controllers/slack/slack-error.utils.js +54 -0
  63. package/dist/backend/backend/src/controllers/slack/slack-error.utils.js.map +1 -0
  64. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  65. package/dist/backend/backend/src/controllers/slack/slack.controller.js +5 -12
  66. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  67. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  68. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  69. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  70. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +9 -0
  72. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  73. package/dist/backend/backend/src/services/google/google-api.client.d.ts +23 -2
  74. package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/google/google-api.client.js +5 -2
  76. package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
  77. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts +61 -11
  78. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/google/google-workspace-token.service.js +108 -31
  80. package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -1
  81. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +24 -0
  82. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +87 -5
  84. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/slack/slack.service.d.ts +17 -0
  86. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/slack/slack.service.js +32 -0
  88. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  89. package/dist/backend/backend/src/types/slack.types.d.ts +10 -0
  90. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  91. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  92. package/dist/backend/backend/src/utils/incomplete-turn.utils.d.ts +1 -1
  93. package/dist/backend/backend/src/utils/incomplete-turn.utils.d.ts.map +1 -1
  94. package/dist/backend/backend/src/utils/incomplete-turn.utils.js +4 -0
  95. package/dist/backend/backend/src/utils/incomplete-turn.utils.js.map +1 -1
  96. package/dist/backend/build-info.json +2 -2
  97. package/dist/cli/backend/src/constants.d.ts +18 -4
  98. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  99. package/dist/cli/backend/src/constants.js +16 -4
  100. package/dist/cli/backend/src/constants.js.map +1 -1
  101. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +24 -0
  102. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  103. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +87 -5
  104. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  105. package/dist/cli/backend/src/services/slack/slack.service.d.ts +17 -0
  106. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  107. package/dist/cli/backend/src/services/slack/slack.service.js +32 -0
  108. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  109. package/dist/cli/backend/src/types/slack.types.d.ts +10 -0
  110. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  111. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  112. package/dist/cli/backend/src/utils/incomplete-turn.utils.d.ts +1 -1
  113. package/dist/cli/backend/src/utils/incomplete-turn.utils.d.ts.map +1 -1
  114. package/dist/cli/backend/src/utils/incomplete-turn.utils.js +4 -0
  115. package/dist/cli/backend/src/utils/incomplete-turn.utils.js.map +1 -1
  116. package/frontend/dist/assets/{index-e079a375.js → index-e7785269.js} +267 -267
  117. package/frontend/dist/index.html +1 -1
  118. package/package.json +1 -1
  119. package/packages/crewly-agent/src/eval/desktop/desktop-tasks.test.ts +96 -0
  120. package/packages/crewly-agent/src/eval/desktop/desktop-tasks.ts +226 -0
  121. package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +10 -1
  122. package/packages/crewly-agent/src/runtime/agent-runner.service.ts +199 -4
  123. package/packages/crewly-agent/src/runtime/computer.tool.test.ts +219 -0
  124. package/packages/crewly-agent/src/runtime/computer.tool.ts +405 -0
  125. package/packages/crewly-agent/src/runtime/desktop-checkpoint.test.ts +136 -0
  126. package/packages/crewly-agent/src/runtime/desktop-checkpoint.ts +231 -0
  127. package/packages/crewly-agent/src/runtime/desktop-recovery.test.ts +100 -0
  128. package/packages/crewly-agent/src/runtime/desktop-recovery.ts +195 -0
  129. package/packages/crewly-agent/src/runtime/desktop-task-runtime.test.ts +251 -0
  130. package/packages/crewly-agent/src/runtime/desktop-task-runtime.ts +423 -0
  131. package/packages/crewly-agent/src/runtime/desktop-task.tool.test.ts +218 -0
  132. package/packages/crewly-agent/src/runtime/desktop-task.tool.ts +343 -0
  133. package/packages/crewly-agent/src/runtime/text-tool-calls.test.ts +144 -0
  134. package/packages/crewly-agent/src/runtime/text-tool-calls.ts +316 -0
  135. package/packages/crewly-agent/src/runtime/text-tool-salvage.test.ts +190 -0
  136. package/packages/crewly-agent/src/runtime/tool-registry.test.ts +17 -0
  137. package/packages/crewly-agent/src/runtime/tool-registry.ts +54 -0
  138. package/packages/crewly-agent/src/runtime/types.ts +16 -1
  139. package/config/skills/agent/vnc-browser/SKILL.md +0 -140
@@ -35,6 +35,14 @@ INPUT=$(read_json_input "${1:-}")
35
35
  ACTION=$(echo "$INPUT" | jq -r '.action // empty')
36
36
  [ -z "$ACTION" ] && error_exit "Missing required parameter: action"
37
37
 
38
+ # ---------------------------------------------------------------------------
39
+ # Safety rails (permissions, stop switch, desktop lock, destructive-key and
40
+ # secure-field refusals, audit) — shared with the marketplace fork.
41
+ # ---------------------------------------------------------------------------
42
+ source "${SCRIPT_DIR}/../../_common/desktop-guards.sh"
43
+ cu_apply_guards
44
+
45
+
38
46
  # ---------------------------------------------------------------------------
39
47
  # Helper: get screen dimensions and scale factor
40
48
  # ---------------------------------------------------------------------------
@@ -56,9 +64,15 @@ get_screen_info() {
56
64
  # optionally overlays a grid, optionally crops.
57
65
  # ---------------------------------------------------------------------------
58
66
  do_screenshot() {
59
- local output="${TMPDIR_CU}/screen_$(date +%s%N).png"
67
+ local output=$(echo "$INPUT" | jq -r '.output // empty')
68
+ [ -z "$output" ] && output="${TMPDIR_CU}/screen_$(date +%s%N).png"
60
69
  local grid=$(echo "$INPUT" | jq -r '.grid // empty')
61
70
  local crop_json=$(echo "$INPUT" | jq -r '.crop // empty')
71
+ # Cap the width the caller wants to reason in. A model's pointing accuracy
72
+ # falls off on a wide image because it is downsampled before the model sees
73
+ # it, and one reasoning in the original coordinate space then points at the
74
+ # wrong place — so the caller asks for the width it will think in.
75
+ local max_width=$(echo "$INPUT" | jq -r '.maxWidth // empty')
62
76
 
63
77
  # Capture full screen (silent)
64
78
  screencapture -x "$output"
@@ -82,6 +96,15 @@ do_screenshot() {
82
96
  local img_h
83
97
  img_h=$(sips -g pixelHeight "$output" | tail -1 | awk '{print $2}')
84
98
 
99
+ # Then, if asked, shrink further to the caller's reasoning width. Never
100
+ # enlarge: a small screen is already easier to point at, and scaling up
101
+ # would invent precision the model does not have.
102
+ if [ -n "$max_width" ] && [ "$max_width" -gt 0 ] 2>/dev/null && [ "$img_w" -gt "$max_width" ]; then
103
+ sips --resampleWidth "$max_width" "$output" --out "$output" >/dev/null 2>&1
104
+ img_w=$(sips -g pixelWidth "$output" | tail -1 | awk '{print $2}')
105
+ img_h=$(sips -g pixelHeight "$output" | tail -1 | awk '{print $2}')
106
+ fi
107
+
85
108
  # Crop FIRST, then grid — so grid labels show absolute screen coordinates
86
109
  local origin_x=0 origin_y=0
87
110
  if [ -n "$crop_json" ]; then
@@ -289,7 +312,9 @@ do_key() {
289
312
  if [ ${#parts[@]} -eq 1 ]; then
290
313
  key_name="${parts[0]}"
291
314
  else
292
- key_name="${parts[-1]}"
315
+ # macOS ships bash 3.2, which has no negative array indices — using one
316
+ # here made every modifier combo fail with "bad array subscript".
317
+ key_name="${parts[$(( ${#parts[@]} - 1 ))]}"
293
318
  for ((i=0; i<${#parts[@]}-1; i++)); do
294
319
  case "${parts[$i]}" in
295
320
  command|cmd) modifiers="${modifiers}command down, " ;;
@@ -844,9 +869,221 @@ do_click_at() {
844
869
  " >/dev/null 2>&1
845
870
  }
846
871
 
872
+ # =============================================================================
873
+ # Element-level perception and action (Phase 2)
874
+ #
875
+ # Everything above works in screen coordinates: the agent takes a screenshot,
876
+ # guesses where a button is, and clicks a point. That misses, costs an image
877
+ # per step, and breaks on a different theme or window position.
878
+ #
879
+ # These work in elements. `snapshot` returns refs (@e1, @e2 …) with roles,
880
+ # names and frames; `click`/`type` accept a ref and go through the
881
+ # accessibility action, which does not care where the window sits or whether
882
+ # something overlaps it.
883
+ # =============================================================================
884
+
885
+ # ---------------------------------------------------------------------------
886
+ # snapshot — the elements of an app, as refs the agent can act on.
887
+ # ---------------------------------------------------------------------------
888
+ do_snapshot() {
889
+ local app max flags=()
890
+ app=$(printf '%s' "$INPUT" | jq -r '.app // empty')
891
+ max=$(printf '%s' "$INPUT" | jq -r '.max // empty')
892
+ [ -n "$app" ] && flags+=(--app "$app")
893
+ [ -n "$max" ] && flags+=(--max "$max")
894
+ [ "$(printf '%s' "$INPUT" | jq -r '.allWindows // false')" = "true" ] && flags+=(--all-windows)
895
+ [ "$(printf '%s' "$INPUT" | jq -r '.menus // false')" = "true" ] && flags+=(--menus)
896
+ cu_perceive snapshot ${flags[@]+"${flags[@]}"}
897
+ }
898
+
899
+ # ---------------------------------------------------------------------------
900
+ # click-ref — press an element by ref.
901
+ #
902
+ # AXPress first: it reaches a button that is scrolled out of view or covered
903
+ # by another window, which a coordinate click cannot. Falls back to clicking
904
+ # the element's centre for the many controls that expose no press action.
905
+ # ---------------------------------------------------------------------------
906
+ do_click_ref() {
907
+ local ref result ok cx cy
908
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
909
+ require_param "ref" "$ref"
910
+
911
+ result=$(cu_perceive press --ref "$ref" 2>&1) || { printf '%s\n' "$result"; exit 1; }
912
+ ok=$(printf '%s' "$result" | jq -r '.success // false')
913
+ if [ "$ok" = "true" ]; then
914
+ printf '%s' "$result" | jq -c '{success:true, action:"click-ref", ref:.ref, method:"AXPress"}'
915
+ return 0
916
+ fi
917
+
918
+ # No press action (or it refused) — click where the element is.
919
+ cx=$(printf '%s' "$result" | jq -r '.center[0] // empty')
920
+ cy=$(printf '%s' "$result" | jq -r '.center[1] // empty')
921
+ if [ -z "$cx" ] || [ -z "$cy" ]; then
922
+ printf '%s\n' "$result"
923
+ exit 1
924
+ fi
925
+ do_click_at "$cx" "$cy"
926
+ jq -n --arg r "$ref" --argjson x "$cx" --argjson y "$cy" \
927
+ '{success:true, action:"click-ref", ref:$r, method:"coordinate-fallback", x:$x, y:$y}'
928
+ }
929
+
930
+ # ---------------------------------------------------------------------------
931
+ # fill-ref — set a field's value directly.
932
+ #
933
+ # More reliable than typing: no dependence on focus, on the keyboard layout,
934
+ # or on an input method being in the right mode.
935
+ # ---------------------------------------------------------------------------
936
+ do_fill_ref() {
937
+ local ref text
938
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
939
+ text=$(printf '%s' "$INPUT" | jq -r '.text // empty')
940
+ require_param "ref" "$ref"
941
+ require_param "text" "$text"
942
+ cu_perceive set-value --ref "$ref" --text "$text"
943
+ }
944
+
945
+ # ---------------------------------------------------------------------------
946
+ # resolve — what is at this ref now.
947
+ #
948
+ # Worth calling after something may have changed the window: it reports
949
+ # whether the element still matches what the snapshot recorded.
950
+ # ---------------------------------------------------------------------------
951
+ do_resolve() {
952
+ local ref
953
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
954
+ require_param "ref" "$ref"
955
+ cu_perceive resolve --ref "$ref"
956
+ }
957
+
958
+ # ---------------------------------------------------------------------------
959
+ # ocr — the text on screen and where it is.
960
+ #
961
+ # Covers what accessibility does not expose: a canvas, a PDF page, an app
962
+ # that simply does not implement AX. Local and free (macOS Vision).
963
+ # ---------------------------------------------------------------------------
964
+ do_ocr() {
965
+ local region flags=()
966
+ region=$(printf '%s' "$INPUT" | jq -r 'if .region then "\(.region.x),\(.region.y),\(.region.w),\(.region.h)" else empty end')
967
+ [ -n "$region" ] && flags+=(--region "$region")
968
+ local image; image=$(printf '%s' "$INPUT" | jq -r '.image // empty')
969
+ [ -n "$image" ] && flags+=(--image "$image")
970
+ cu_perceive ocr ${flags[@]+"${flags[@]}"}
971
+ }
972
+
973
+ # ---------------------------------------------------------------------------
974
+ # displays — every screen and its frame, so coordinates are unambiguous.
975
+ # ---------------------------------------------------------------------------
976
+ do_displays() {
977
+ cu_perceive displays
978
+ }
979
+
980
+ # ---------------------------------------------------------------------------
981
+ # wait-for — block until the screen is in the expected state.
982
+ #
983
+ # The alternative is sleeping and hoping: an agent that clicks during an
984
+ # animation hits nothing, and one that sleeps long enough to be safe is slow
985
+ # on every step instead.
986
+ #
987
+ # {"action":"wait-for","app":"Numbers"} that app is frontmost
988
+ # {"action":"wait-for","ref":"@e12"} that element resolves
989
+ # {"action":"wait-for","text":"Export"} that text is on screen
990
+ # {"action":"wait-for","idle":true} the screen stopped changing
991
+ # ---------------------------------------------------------------------------
992
+ do_wait_for() {
993
+ local app ref text idle timeout deadline
994
+ app=$(printf '%s' "$INPUT" | jq -r '.app // empty')
995
+ ref=$(printf '%s' "$INPUT" | jq -r '.ref // empty')
996
+ text=$(printf '%s' "$INPUT" | jq -r '.text // empty')
997
+ idle=$(printf '%s' "$INPUT" | jq -r '.idle // false')
998
+ timeout=$(printf '%s' "$INPUT" | jq -r '.timeoutMs // 10000')
999
+ deadline=$(( $(date +%s) + timeout / 1000 ))
1000
+
1001
+ if [ -z "$app" ] && [ -z "$ref" ] && [ -z "$text" ] && [ "$idle" != "true" ]; then
1002
+ error_exit "wait-for needs one of: app, ref, text, idle"
1003
+ fi
1004
+
1005
+ local previous="" current
1006
+ while [ "$(date +%s)" -le "$deadline" ]; do
1007
+ if [ -n "$app" ]; then
1008
+ local front
1009
+ front=$(osascript -e 'tell application "System Events" to get name of first process whose frontmost is true' 2>/dev/null)
1010
+ if [ "$front" = "$app" ]; then
1011
+ jq -n --arg a "$app" '{success:true, action:"wait-for", matched:"app", app:$a}'; return 0
1012
+ fi
1013
+ fi
1014
+ if [ -n "$ref" ]; then
1015
+ if cu_perceive resolve --ref "$ref" 2>/dev/null | jq -e '.success and .matches' >/dev/null 2>&1; then
1016
+ jq -n --arg r "$ref" '{success:true, action:"wait-for", matched:"ref", ref:$r}'; return 0
1017
+ fi
1018
+ fi
1019
+ if [ -n "$text" ]; then
1020
+ if cu_perceive ocr 2>/dev/null | jq -e --arg t "$text" '[.items[]?.text | select(contains($t))] | length > 0' >/dev/null 2>&1; then
1021
+ jq -n --arg t "$text" '{success:true, action:"wait-for", matched:"text", text:$t}'; return 0
1022
+ fi
1023
+ fi
1024
+ if [ "$idle" = "true" ]; then
1025
+ # Two consecutive identical frames mean nothing is animating.
1026
+ local shot="${TMPDIR_CU}/idle-$$.png"
1027
+ screencapture -x -t jpg "$shot" 2>/dev/null
1028
+ current=$(shasum -a 1 "$shot" 2>/dev/null | cut -d' ' -f1)
1029
+ rm -f "$shot"
1030
+ if [ -n "$previous" ] && [ "$current" = "$previous" ]; then
1031
+ jq -n '{success:true, action:"wait-for", matched:"idle"}'; return 0
1032
+ fi
1033
+ previous="$current"
1034
+ fi
1035
+ sleep 0.4
1036
+ done
1037
+
1038
+ cu_fail "wait_timeout" \
1039
+ "Nothing matched within ${timeout}ms. Take a snapshot to see what is actually on screen." \
1040
+ "$(jq -n --argjson t "$timeout" '{timeoutMs:$t}')"
1041
+ }
1042
+
1043
+ # ---------------------------------------------------------------------------
1044
+ # request-human — hand the machine back for something an agent must not do.
1045
+ #
1046
+ # The deleted `vnc-browser` skill described this and shipped no script. The
1047
+ # honest version is smaller than a VNC tunnel and covers the cases that
1048
+ # actually arise: a CAPTCHA, a two-factor prompt, a login, a decision the
1049
+ # owner has to make. The agent pauses itself, says what it needs and why, and
1050
+ # waits — rather than trying to be clever, which for a login means typing a
1051
+ # password it must never type.
1052
+ #
1053
+ # Pausing (not stopping) is the point: the owner deals with it and the task
1054
+ # picks up, using the same banner button they already have.
1055
+ # ---------------------------------------------------------------------------
1056
+ do_request_human() {
1057
+ local reason detail shot
1058
+ reason=$(printf '%s' "$INPUT" | jq -r '.reason // empty')
1059
+ detail=$(printf '%s' "$INPUT" | jq -r '.detail // empty')
1060
+ require_param "reason" "$reason"
1061
+
1062
+ # A picture of what the agent is stuck on is worth more than its
1063
+ # description of it, and the owner may be reading this on a phone.
1064
+ shot=$(capture_thumb handover 2>/dev/null || true)
1065
+
1066
+ # Pause rather than stop: the task is not cancelled, it is waiting.
1067
+ printf 'awaiting-human\n' > "$DESKTOP_PAUSE" 2>/dev/null || true
1068
+ cu_presence begin --agent "$HOLDER" --goal "needs you: $reason"
1069
+
1070
+ jq -n --arg r "$reason" --arg d "$detail" --arg s "$HOLDER" --arg shot "$shot" \
1071
+ '{success:true, action:"request-human", waiting:true, reason:$r,
1072
+ detail:$d, agent:$s,
1073
+ note:"Desktop control is paused until the owner resumes it (banner → Resume, or remove ~/.crewly/desktop.pause). Do not try to work around this."}
1074
+ + (if $shot == "" then {} else {screenshot:$shot} end)'
1075
+ }
1076
+
847
1077
  # ---------------------------------------------------------------------------
848
1078
  # Action dispatch
1079
+ #
1080
+ # The "after" audit shot is taken on the way out, so the log holds a pair
1081
+ # showing what the action actually changed — the point of keeping evidence at
1082
+ # all. It runs on every exit path, including a failure, because a failed
1083
+ # action that moved something is exactly what one wants to look at later.
849
1084
  # ---------------------------------------------------------------------------
1085
+ trap 'cu_log_after 2>/dev/null || true' EXIT
1086
+
850
1087
  case "$ACTION" in
851
1088
  screenshot) do_screenshot ;;
852
1089
  click) do_click ;;
@@ -860,5 +1097,14 @@ case "$ACTION" in
860
1097
  list-apps) do_list_apps ;;
861
1098
  find) do_find ;;
862
1099
  click-text) do_click_text ;;
863
- *) error_exit "Unknown action: $ACTION. Valid: screenshot, click, move, type, key, scroll, drag, focus, open-url, list-apps, find, click-text" ;;
1100
+ check-permissions) do_check_permissions ;;
1101
+ snapshot) do_snapshot ;;
1102
+ click-ref) do_click_ref ;;
1103
+ fill-ref) do_fill_ref ;;
1104
+ resolve) do_resolve ;;
1105
+ ocr) do_ocr ;;
1106
+ displays) do_displays ;;
1107
+ wait-for) do_wait_for ;;
1108
+ request-human) do_request_human ;;
1109
+ *) error_exit "Unknown action: $ACTION. Valid: screenshot, click, move, type, key, scroll, drag, focus, open-url, list-apps, find, click-text, check-permissions, snapshot, click-ref, fill-ref, resolve, ocr, displays, wait-for, request-human" ;;
864
1110
  esac
@@ -60,3 +60,13 @@ bash execute.sh '{"summary":"Standup","start":"2026-09-21T09:00:00Z","end":"2026
60
60
  Missing `--summary` / `--start` / `--end` → `{"error":"Missing required parameter: …"}` on stderr, exit 1.
61
61
  `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1) when the owner has not connected Google Workspace;
62
62
  `reason` `validation` when Google cannot parse a time.
63
+
64
+ ## Choosing a Google account
65
+
66
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
67
+
68
+ ```bash
69
+ bash execute.sh --account work@company.com ...
70
+ ```
71
+
72
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -28,6 +28,7 @@ Options:
28
28
  --description Body text
29
29
  --attendee Attendee email (repeat for several)
30
30
  --calendar Calendar id (default: primary)
31
+ --account Which connected Google account to act as (default: your primary)
31
32
  --help | -h Show this help
32
33
  EOF_USAGE
33
34
  }
@@ -41,6 +42,7 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
41
42
  shift || true
42
43
  fi
43
44
 
45
+ ACCOUNT=""
44
46
  while [[ $# -gt 0 ]]; do
45
47
  case "$1" in
46
48
  --summary|--title) [ $# -ge 2 ] || error_exit "--summary requires a value"; SUMMARY="$2"; shift 2 ;;
@@ -51,6 +53,7 @@ while [[ $# -gt 0 ]]; do
51
53
  --calendar|-c) [ $# -ge 2 ] || error_exit "--calendar requires a value"; CALENDAR="$2"; shift 2 ;;
52
54
  --attendee) [ $# -ge 2 ] || error_exit "--attendee requires a value"
53
55
  ATTENDEES_JSON=$(jq -cn --argjson arr "$ATTENDEES_JSON" --arg v "$2" '$arr + [$v]'); shift 2 ;;
56
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
54
57
  --help|-h) print_usage; exit 0 ;;
55
58
  *) error_exit "Unknown option: $1" ;;
56
59
  esac
@@ -67,7 +70,10 @@ if [ -n "$INPUT_JSON" ]; then
67
70
  if [ "$(printf '%s' "$ATTENDEES_JSON" | jq 'length')" -eq 0 ]; then
68
71
  ATTENDEES_JSON=$(printf '%s' "$INPUT" | jq -c '(.attendees // []) | if type == "string" then split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(. != "")) else . end')
69
72
  fi
73
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
70
74
  fi
75
+ # Route the call at one connected Google account; unset means the default.
76
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
71
77
 
72
78
  require_param "summary (--summary)" "$SUMMARY"
73
79
  require_param "start (--start)" "$START"
@@ -56,3 +56,13 @@ bash execute.sh '{"calendarId":"team@group.calendar.google.com","from":"…","to
56
56
 
57
57
  `{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
58
58
  when the owner has not connected Google Workspace.
59
+
60
+ ## Choosing a Google account
61
+
62
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
63
+
64
+ ```bash
65
+ bash execute.sh --account work@company.com ...
66
+ ```
67
+
68
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -24,6 +24,7 @@ Options:
24
24
  --to Upper bound (default: now + 7 days)
25
25
  --calendar Calendar id (default: primary)
26
26
  --max Result cap (default 50, max 250)
27
+ --account Which connected Google account to act as (default: your primary)
27
28
  --help | -h Show this help
28
29
  EOF_USAGE
29
30
  }
@@ -48,12 +49,14 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
48
49
  shift || true
49
50
  fi
50
51
 
52
+ ACCOUNT=""
51
53
  while [[ $# -gt 0 ]]; do
52
54
  case "$1" in
53
55
  --from) [ $# -ge 2 ] || error_exit "--from requires a value"; FROM="$2"; shift 2 ;;
54
56
  --to) [ $# -ge 2 ] || error_exit "--to requires a value"; TO="$2"; shift 2 ;;
55
57
  --calendar|-c) [ $# -ge 2 ] || error_exit "--calendar requires a value"; CALENDAR="$2"; shift 2 ;;
56
58
  --max|-n) [ $# -ge 2 ] || error_exit "--max requires a value"; MAX="$2"; shift 2 ;;
59
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
57
60
  --help|-h) print_usage; exit 0 ;;
58
61
  *) error_exit "Unknown option: $1" ;;
59
62
  esac
@@ -65,7 +68,10 @@ if [ -n "$INPUT_JSON" ]; then
65
68
  [ -z "$TO" ] && TO=$(printf '%s' "$INPUT" | jq -r '.to // .timeMax // empty')
66
69
  [ -z "$CALENDAR" ] && CALENDAR=$(printf '%s' "$INPUT" | jq -r '.calendarId // .calendar // empty')
67
70
  [ -z "$MAX" ] && MAX=$(printf '%s' "$INPUT" | jq -r '.max // empty')
71
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
68
72
  fi
73
+ # Route the call at one connected Google account; unset means the default.
74
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
69
75
 
70
76
  [ -z "$FROM" ] && FROM=$(date -u +%Y-%m-%dT%H:%M:%SZ)
71
77
  [ -z "$TO" ] && TO=$(iso_now_plus_days "$DEFAULT_WINDOW_DAYS")
@@ -48,3 +48,13 @@ bash execute.sh --id "https://docs.google.com/document/d/1AbC…/edit"
48
48
  when the owner has not connected Google Workspace (Settings → Integrations).
49
49
  A `403` with `reason: "google_error"` on a write means the file was not
50
50
  created by Crewly — the grant only edits files Crewly made (`drive.file`).
51
+
52
+ ## Choosing a Google account
53
+
54
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
55
+
56
+ ```bash
57
+ bash execute.sh --account work@company.com ...
58
+ ```
59
+
60
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -20,6 +20,7 @@ Usage:
20
20
 
21
21
  Options:
22
22
  --id Document id (from drive-search or the docs.google.com URL)
23
+ --account Which connected Google account to act as (default: your primary)
23
24
  --help | -h Show this help
24
25
  EOF_USAGE
25
26
  }
@@ -48,10 +49,11 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
48
49
  INPUT_JSON="$1"
49
50
  shift || true
50
51
  fi
51
- ID=""
52
+ ID=""; ACCOUNT=""
52
53
  while [[ $# -gt 0 ]]; do
53
54
  case "$1" in
54
55
  --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
56
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
55
57
  --help|-h) print_usage; exit 0 ;;
56
58
  *) error_exit "Unknown option: $1" ;;
57
59
  esac
@@ -59,7 +61,10 @@ done
59
61
  if [ -n "$INPUT_JSON" ]; then
60
62
  INPUT=$(read_json_input "$INPUT_JSON")
61
63
  [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .documentId // empty')
64
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
62
65
  fi
66
+ # Route the call at one connected Google account; unset means the default.
67
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
63
68
  # Accept a full docs.google.com URL too.
64
69
  ID=$(printf '%s' "$ID" | sed -E 's#.*/document/d/([^/?]+).*#\1#')
65
70
  require_param "id (--id)" "$ID"
@@ -53,3 +53,13 @@ re-connect Google Workspace once the scope is enabled".
53
53
  when the owner has not connected Google Workspace (Settings → Integrations).
54
54
  A `403` with `reason: "google_error"` on a write means the file was not
55
55
  created by Crewly — the grant only edits files Crewly made (`drive.file`).
56
+
57
+ ## Choosing a Google account
58
+
59
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
60
+
61
+ ```bash
62
+ bash execute.sh --account work@company.com ...
63
+ ```
64
+
65
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -27,6 +27,7 @@ Options:
27
27
  --id Append to this document instead (must be one Crewly created, or the grant needs the Docs scope)
28
28
  --text Body text (plain; blank line = new paragraph)
29
29
  --text-file Read the body from a file
30
+ --account Which connected Google account to act as (default: your primary)
30
31
  --help | -h Show this help
31
32
  EOF_USAGE
32
33
  }
@@ -55,13 +56,14 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
55
56
  INPUT_JSON="$1"
56
57
  shift || true
57
58
  fi
58
- TITLE=""; ID=""; TEXT=""; TEXT_FILE=""
59
+ TITLE=""; ID=""; TEXT=""; TEXT_FILE=""; ACCOUNT=""
59
60
  while [[ $# -gt 0 ]]; do
60
61
  case "$1" in
61
62
  --title) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
62
63
  --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
63
64
  --text) [ $# -ge 2 ] || error_exit "--text requires a value"; TEXT="$2"; shift 2 ;;
64
65
  --text-file) [ $# -ge 2 ] || error_exit "--text-file requires a value"; TEXT_FILE="$2"; shift 2 ;;
66
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
65
67
  --help|-h) print_usage; exit 0 ;;
66
68
  *) error_exit "Unknown option: $1" ;;
67
69
  esac
@@ -71,7 +73,10 @@ if [ -n "$INPUT_JSON" ]; then
71
73
  [ -z "$TITLE" ] && TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
72
74
  [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .documentId // empty')
73
75
  [ -z "$TEXT" ] && TEXT=$(printf '%s' "$INPUT" | jq -r '.text // .body // empty')
76
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
74
77
  fi
78
+ # Route the call at one connected Google account; unset means the default.
79
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
75
80
  if [ -n "$TEXT_FILE" ]; then
76
81
  [ -f "$TEXT_FILE" ] || error_exit "text file not found: $TEXT_FILE"
77
82
  TEXT=$(cat "$TEXT_FILE")
@@ -51,3 +51,13 @@ Files over 10 MB are refused with a `webViewLink` to open instead.
51
51
  when the owner has not connected Google Workspace (Settings → Integrations).
52
52
  A `403` with `reason: "google_error"` on a write means the file was not
53
53
  created by Crewly — the grant only edits files Crewly made (`drive.file`).
54
+
55
+ ## Choosing a Google account
56
+
57
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
58
+
59
+ ```bash
60
+ bash execute.sh --account work@company.com ...
61
+ ```
62
+
63
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -21,6 +21,7 @@ Usage:
21
21
  Options:
22
22
  --id Drive file id (required; from drive-search)
23
23
  --out Save the content to this path instead of printing it (binary files are decoded)
24
+ --account Which connected Google account to act as (default: your primary)
24
25
  --help | -h Show this help
25
26
  EOF_USAGE
26
27
  }
@@ -49,11 +50,12 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
49
50
  INPUT_JSON="$1"
50
51
  shift || true
51
52
  fi
52
- ID=""; OUT_PATH=""
53
+ ID=""; OUT_PATH=""; ACCOUNT=""
53
54
  while [[ $# -gt 0 ]]; do
54
55
  case "$1" in
55
56
  --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
56
57
  --out|-o) [ $# -ge 2 ] || error_exit "--out requires a value"; OUT_PATH="$2"; shift 2 ;;
58
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
57
59
  --help|-h) print_usage; exit 0 ;;
58
60
  *) error_exit "Unknown option: $1" ;;
59
61
  esac
@@ -62,7 +64,10 @@ if [ -n "$INPUT_JSON" ]; then
62
64
  INPUT=$(read_json_input "$INPUT_JSON")
63
65
  [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .fileId // empty')
64
66
  [ -z "$OUT_PATH" ] && OUT_PATH=$(printf '%s' "$INPUT" | jq -r '.out // empty')
67
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
65
68
  fi
69
+ # Route the call at one connected Google account; unset means the default.
70
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
66
71
  require_param "id (--id)" "$ID"
67
72
  RESPONSE=$(call GET "/google/drive/files/$(uri "$ID")/content") || { printf '%s\n' "$RESPONSE"; exit 1; }
68
73
  if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
@@ -53,3 +53,13 @@ else → `drive-read`.
53
53
  when the owner has not connected Google Workspace (Settings → Integrations).
54
54
  A `403` with `reason: "google_error"` on a write means the file was not
55
55
  created by Crewly — the grant only edits files Crewly made (`drive.file`).
56
+
57
+ ## Choosing a Google account
58
+
59
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
60
+
61
+ ```bash
62
+ bash execute.sh --account work@company.com ...
63
+ ```
64
+
65
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -23,6 +23,7 @@ Options:
23
23
  --mime Restrict to a type: doc | sheet | slides | pdf | folder | any MIME type
24
24
  --folder Restrict to a Drive folder id
25
25
  --max Result cap (default 20, max 100)
26
+ --account Which connected Google account to act as (default: your primary)
26
27
  --help | -h Show this help
27
28
  EOF_USAGE
28
29
  }
@@ -51,13 +52,14 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
51
52
  INPUT_JSON="$1"
52
53
  shift || true
53
54
  fi
54
- QUERY=""; MIME=""; FOLDER=""; MAX=""
55
+ QUERY=""; MIME=""; FOLDER=""; MAX=""; ACCOUNT=""
55
56
  while [[ $# -gt 0 ]]; do
56
57
  case "$1" in
57
58
  --query|-q) [ $# -ge 2 ] || error_exit "--query requires a value"; QUERY="$2"; shift 2 ;;
58
59
  --mime) [ $# -ge 2 ] || error_exit "--mime requires a value"; MIME="$2"; shift 2 ;;
59
60
  --folder) [ $# -ge 2 ] || error_exit "--folder requires a value"; FOLDER="$2"; shift 2 ;;
60
61
  --max|-n) [ $# -ge 2 ] || error_exit "--max requires a value"; MAX="$2"; shift 2 ;;
62
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
61
63
  --help|-h) print_usage; exit 0 ;;
62
64
  *) error_exit "Unknown option: $1" ;;
63
65
  esac
@@ -68,7 +70,10 @@ if [ -n "$INPUT_JSON" ]; then
68
70
  [ -z "$MIME" ] && MIME=$(printf '%s' "$INPUT" | jq -r '.mime // .mimeType // empty')
69
71
  [ -z "$FOLDER" ] && FOLDER=$(printf '%s' "$INPUT" | jq -r '.folder // .folderId // empty')
70
72
  [ -z "$MAX" ] && MAX=$(printf '%s' "$INPUT" | jq -r '.max // empty')
73
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
71
74
  fi
75
+ # Route the call at one connected Google account; unset means the default.
76
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
72
77
  case "$MIME" in
73
78
  doc|docs) MIME="application/vnd.google-apps.document" ;;
74
79
  sheet|sheets) MIME="application/vnd.google-apps.spreadsheet" ;;
@@ -52,3 +52,13 @@ permissions are the owner's — this skill does not change sharing).
52
52
  when the owner has not connected Google Workspace (Settings → Integrations).
53
53
  A `403` with `reason: "google_error"` on a write means the file was not
54
54
  created by Crewly — the grant only edits files Crewly made (`drive.file`).
55
+
56
+ ## Choosing a Google account
57
+
58
+ Several Google accounts can be connected at once. Without `--account` the call uses the default one (the first you connected, or whichever you marked default on the Connections page). Name one explicitly when it matters:
59
+
60
+ ```bash
61
+ bash execute.sh --account work@company.com ...
62
+ ```
63
+
64
+ The account must be connected *for this product* — Google consent is per product (Gmail / Calendar / Drive), so an account connected only for Calendar cannot read Drive.
@@ -28,6 +28,7 @@ Options:
28
28
  --mime MIME type override
29
29
  --folder Destination folder id (default: My Drive root)
30
30
  --convert Convert on upload: doc | sheet | slides (e.g. a .docx/.csv/.pptx becomes a Google-native file)
31
+ --account Which connected Google account to act as (default: your primary)
31
32
  --help | -h Show this help
32
33
  EOF_USAGE
33
34
  }
@@ -56,7 +57,7 @@ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
56
57
  INPUT_JSON="$1"
57
58
  shift || true
58
59
  fi
59
- FILE=""; NAME=""; TEXT=""; MIME=""; FOLDER=""; CONVERT=""
60
+ FILE=""; NAME=""; TEXT=""; MIME=""; FOLDER=""; CONVERT=""; ACCOUNT=""
60
61
  while [[ $# -gt 0 ]]; do
61
62
  case "$1" in
62
63
  --path|-p) [ $# -ge 2 ] || error_exit "--path requires a value"; FILE="$2"; shift 2 ;;
@@ -65,6 +66,7 @@ while [[ $# -gt 0 ]]; do
65
66
  --mime) [ $# -ge 2 ] || error_exit "--mime requires a value"; MIME="$2"; shift 2 ;;
66
67
  --folder) [ $# -ge 2 ] || error_exit "--folder requires a value"; FOLDER="$2"; shift 2 ;;
67
68
  --convert) [ $# -ge 2 ] || error_exit "--convert requires a value"; CONVERT="$2"; shift 2 ;;
69
+ --account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
68
70
  --help|-h) print_usage; exit 0 ;;
69
71
  *) error_exit "Unknown option: $1" ;;
70
72
  esac
@@ -77,7 +79,10 @@ if [ -n "$INPUT_JSON" ]; then
77
79
  [ -z "$MIME" ] && MIME=$(printf '%s' "$INPUT" | jq -r '.mime // .mimeType // empty')
78
80
  [ -z "$FOLDER" ] && FOLDER=$(printf '%s' "$INPUT" | jq -r '.folder // .folderId // empty')
79
81
  [ -z "$CONVERT" ] && CONVERT=$(printf '%s' "$INPUT" | jq -r '.convert // .convertTo // empty')
82
+ [ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
80
83
  fi
84
+ # Route the call at one connected Google account; unset means the default.
85
+ [ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
81
86
  case "$CONVERT" in
82
87
  doc|docs) CONVERT="application/vnd.google-apps.document" ;;
83
88
  sheet|sheets) CONVERT="application/vnd.google-apps.spreadsheet" ;;