crewly 1.20.281 → 1.20.283

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 (57) hide show
  1. package/config/skills/agent/core/receive-file/SKILL.md +53 -0
  2. package/config/skills/agent/core/receive-file/execute.sh +125 -0
  3. package/config/skills/agent/core/receive-file/execute.test.sh +80 -0
  4. package/config/skills/agent/core/send-file/SKILL.md +62 -0
  5. package/config/skills/agent/core/send-file/execute.sh +145 -0
  6. package/config/skills/agent/core/send-file/execute.test.sh +81 -0
  7. package/dist/backend/backend/src/constants.d.ts +17 -0
  8. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  9. package/dist/backend/backend/src/constants.js +17 -0
  10. package/dist/backend/backend/src/constants.js.map +1 -1
  11. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +12 -0
  12. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  13. package/dist/backend/backend/src/controllers/apps/apps.controller.js +41 -0
  14. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  15. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +4 -0
  16. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  17. package/dist/backend/backend/src/controllers/apps/apps.routes.js +12 -1
  18. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  19. package/dist/backend/backend/src/services/agent/antigravity-stuck-command.d.ts +74 -0
  20. package/dist/backend/backend/src/services/agent/antigravity-stuck-command.d.ts.map +1 -0
  21. package/dist/backend/backend/src/services/agent/antigravity-stuck-command.js +101 -0
  22. package/dist/backend/backend/src/services/agent/antigravity-stuck-command.js.map +1 -0
  23. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  24. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  25. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  26. package/dist/backend/backend/src/services/harness/harness-relogin.service.js +1 -1
  27. package/dist/backend/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  28. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts +8 -0
  29. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +30 -1
  31. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.d.ts +22 -0
  33. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.d.ts.map +1 -1
  34. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.js +81 -0
  35. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.js.map +1 -1
  36. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js +9 -0
  38. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js.map +1 -1
  39. package/dist/backend/build-info.json +2 -2
  40. package/dist/cli/backend/src/constants.d.ts +17 -0
  41. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  42. package/dist/cli/backend/src/constants.js +17 -0
  43. package/dist/cli/backend/src/constants.js.map +1 -1
  44. package/dist/cli/backend/src/services/agent/antigravity-stuck-command.d.ts +74 -0
  45. package/dist/cli/backend/src/services/agent/antigravity-stuck-command.d.ts.map +1 -0
  46. package/dist/cli/backend/src/services/agent/antigravity-stuck-command.js +101 -0
  47. package/dist/cli/backend/src/services/agent/antigravity-stuck-command.js.map +1 -0
  48. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  49. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  50. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  51. package/dist/cli/backend/src/services/harness/harness-relogin.service.js +1 -1
  52. package/dist/cli/backend/src/services/harness/harness-relogin.service.js.map +1 -1
  53. package/dist/cli/backend/src/services/monitoring/activity-monitor.service.d.ts +8 -0
  54. package/dist/cli/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
  55. package/dist/cli/backend/src/services/monitoring/activity-monitor.service.js +30 -1
  56. package/dist/cli/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
  57. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Receive File
3
+ description: Download a large file another agent of the same Crewly account sent with send-file (handoff id hf_…), save it in your workspace, verify its size, then tell Crewly Cloud to delete it. Use when a message gives you a handoff id or says "receive it with the receive-file skill".
4
+ version: 1.0.0
5
+ category: productivity
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - receive file
11
+ - download handoff
12
+ - hf_
13
+ - get the file from
14
+ - fetch the video
15
+ tags:
16
+ - files
17
+ - handoff
18
+ - transfer
19
+ execution:
20
+ type: script
21
+ script:
22
+ file: execute.sh
23
+ interpreter: bash
24
+ timeoutMs: 3600000
25
+ ---
26
+
27
+ # Receive File
28
+
29
+ Downloads a file handed to you with `send-file` (by any agent of your own
30
+ Crewly account, on this or another machine). The file streams to disk, never
31
+ through memory; its size is checked against what the sender declared; then the
32
+ temporary copy on the server is deleted.
33
+
34
+ ```bash
35
+ bash execute.sh --id hf_AbCdEfGhIjKlMnOpQrStUv
36
+ bash execute.sh --id hf_… --dir ./inbox
37
+ ```
38
+
39
+ Prints:
40
+
41
+ ```json
42
+ {"success":true,"path":"/…/received/daily-2026-10-09.mp4","fileName":"daily-2026-10-09.mp4","sizeBytes":314572800,"note":"today's cut","acked":true}
43
+ ```
44
+
45
+ - `--dir`: where to save (default `received/` in your project directory).
46
+ An existing file is never overwritten; a number is added to the name.
47
+ - If the size does not match, nothing is acknowledged and the partial file is
48
+ removed: run it again (the server copy stays until it is acknowledged or
49
+ 24 hours pass).
50
+ - An interrupted download keeps its `.part` file and resumes on the next run.
51
+ - `--no-ack` keeps the server copy so another run can fetch it again.
52
+ - A handoff is gone once received, cancelled or expired (24 h): ask the sender
53
+ to send it again.
@@ -0,0 +1,125 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # receive-file — download a file handed over with send-file, verify its size,
4
+ # acknowledge it (Cloud then deletes it). crewly-services apps/SPEC.md section 18.
5
+ #
6
+ # The backend (GET /api/apps/handoffs/:id) answers metadata and a presigned
7
+ # download URL; curl streams it straight to disk (resumable via a .part file).
8
+ #
9
+ # Usage:
10
+ # bash execute.sh --id hf_… [--dir <folder>] [--no-ack]
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 hf_… [--dir <folder>] [--no-ack]
20
+
21
+ Options:
22
+ --id Handoff id from the sender (required)
23
+ --dir Folder to save into (default: received/ in your project directory)
24
+ --no-ack Do not delete the server copy afterwards
25
+ --help | -h Show this help
26
+ EOF_USAGE
27
+ }
28
+
29
+ fail_from() {
30
+ printf '%s' "$1" | jq -c '{success: false, status: (.status // 0), reason: (.details.error // .error // "unknown"), message: (.details.message // .details // "")}' 2>/dev/null \
31
+ || jq -cn --arg r "$1" '{success: false, reason: $r}'
32
+ exit 1
33
+ }
34
+
35
+ call() {
36
+ local out
37
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
38
+ printf '%s\n' "$out" | tail -n 1
39
+ }
40
+
41
+ ID=""; DIR=""; ACK=1
42
+ while [[ $# -gt 0 ]]; do
43
+ case "$1" in
44
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
45
+ --dir) [ $# -ge 2 ] || error_exit "--dir requires a value"; DIR="$2"; shift 2 ;;
46
+ --no-ack) ACK=0; shift ;;
47
+ --help|-h) print_usage; exit 0 ;;
48
+ *) error_exit "Unknown option: $1" ;;
49
+ esac
50
+ done
51
+ [ -n "$ID" ] || error_exit "--id is required"
52
+ [[ "$ID" =~ ^hf_[A-Za-z0-9_-]{22}$ ]] || error_exit "--id must be a handoff id like hf_… (22 characters after hf_)"
53
+ [ -n "$DIR" ] || DIR="${CREWLY_PROJECT_PATH:-$PWD}/received"
54
+ command -v node >/dev/null 2>&1 || error_exit "node is required"
55
+
56
+ # The destination folder must not be inside the Crewly home directory.
57
+ check_dir() {
58
+ RF_DIR="$1" node <<'NODE'
59
+ const fs = require('fs'); const path = require('path'); const os = require('os');
60
+ const abs = path.resolve(process.env.RF_DIR);
61
+ fs.mkdirSync(abs, { recursive: true });
62
+ const r = fs.realpathSync(abs);
63
+ const homes = [path.join(os.homedir(), '.crewly'), process.env.CREWLY_HOME].filter(Boolean).map((p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } });
64
+ if (homes.some((h) => r === h || r.startsWith(h + path.sep))) { process.stderr.write(JSON.stringify({ error: 'the folder is inside the Crewly home directory' }) + '\n'); process.exit(1); }
65
+ process.stdout.write(r);
66
+ NODE
67
+ }
68
+ DIR=$(check_dir "$DIR") || exit 1
69
+
70
+ META=$(call GET "/apps/handoffs/${ID}") || { printf '%s\n' "$META"; exit 1; }
71
+ STATUS=$(printf '%s' "$META" | jq -r '.data.status // empty')
72
+ if [ "$STATUS" != "ready" ]; then
73
+ jq -cn '{success: false, reason: "not_ready", message: "The sender has not finished uploading. Ask them to confirm, then run this again."}'
74
+ exit 1
75
+ fi
76
+ URL=$(printf '%s' "$META" | jq -r '.data.download.url // empty')
77
+ SIZE=$(printf '%s' "$META" | jq -r '.data.sizeBytes // empty')
78
+ RAW_NAME=$(printf '%s' "$META" | jq -r '.data.fileName // "file"')
79
+ NOTE=$(printf '%s' "$META" | jq -r '.data.note // empty')
80
+ [ -n "$URL" ] && [[ "$SIZE" =~ ^[0-9]+$ ]] || error_exit "Cloud answered without a download URL"
81
+
82
+ # A safe local name: the last path segment, no leading dots.
83
+ NAME=$(printf '%s' "$RAW_NAME" | tr '\\' '/' | awk -F/ '{print $NF}' | sed 's/^[.[:space:]]*//')
84
+ [ -n "$NAME" ] || NAME="file"
85
+
86
+ # The .part file is named by handoff id so an interrupted run resumes.
87
+ PART="${DIR}/.${ID}.part"
88
+ if [ -f "$PART" ]; then
89
+ HAVE=$(wc -c < "$PART" | tr -d ' ')
90
+ [ "$HAVE" -le "$SIZE" ] || rm -f "$PART"
91
+ fi
92
+ CODE=$(curl -sS -o "$PART" -w '%{http_code}' --connect-timeout 30 --retry 3 --retry-delay 3 -C - "$URL" 2>/dev/null) || CODE="000"
93
+ # 200 = whole body, 206 = resumed, 416 = the .part was already complete.
94
+ case "$CODE" in 200|206|416) ;; *)
95
+ [ "$CODE" = "000" ] || rm -f "$PART"
96
+ jq -cn --arg c "$CODE" '{success: false, reason: "download_failed", message: ("The download failed (HTTP " + $c + "). Run the skill again; a partial file is resumed.")}'
97
+ exit 1 ;;
98
+ esac
99
+
100
+ GOT=$(wc -c < "$PART" | tr -d ' ')
101
+ if [ "$GOT" != "$SIZE" ]; then
102
+ rm -f "$PART"
103
+ jq -cn --argjson g "$GOT" --argjson s "$SIZE" '{success: false, reason: "size_mismatch", message: ("Downloaded " + ($g|tostring) + " bytes, expected " + ($s|tostring) + ". The partial file was removed and the server copy kept; run the skill again.")}'
104
+ exit 1
105
+ fi
106
+
107
+ # Final name: never overwrite.
108
+ DEST="${DIR}/${NAME}"
109
+ if [ -e "$DEST" ]; then
110
+ BASE="${NAME%.*}"; EXT=""
111
+ [ "$BASE" != "$NAME" ] && EXT=".${NAME##*.}"
112
+ N=1
113
+ while [ -e "${DIR}/${BASE}-${N}${EXT}" ]; do N=$((N+1)); done
114
+ DEST="${DIR}/${BASE}-${N}${EXT}"
115
+ fi
116
+ mv "$PART" "$DEST"
117
+
118
+ ACKED=false
119
+ if [ "$ACK" = "1" ]; then
120
+ if api_call POST "/apps/handoffs/${ID}/ack" >/dev/null 2>&1; then ACKED=true; fi
121
+ fi
122
+
123
+ jq -cn --arg p "$DEST" --arg n "$NAME" --argjson s "$SIZE" --arg note "$NOTE" --argjson a "$ACKED" \
124
+ '{success: true, path: $p, fileName: $n, sizeBytes: $s, acked: $a} + (if $note != "" then {note: $note} else {} end)
125
+ + (if $a then {} else {warning: "The server copy was not deleted (it is removed automatically after 24 hours)."} end)'
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # Tests for receive-file — run with: bash execute.test.sh
3
+ # A Python stub plays both the Crewly backend and the object storage.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() { if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi; }
9
+
10
+ PORT=18862
11
+ STUB_LOG="$(mktemp)"; export STUB_LOG STUB_PORT="$PORT"
12
+ python3 -u <<'PY' >/dev/null 2>&1 &
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ import json, os
15
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
16
+ OK = 'hf_AbCdEfGhIjKlMnOpQrStUv'; SHORT = 'hf_ShortShortShortShortSh'; PEND = 'hf_PendPendPendPendPendPe'
17
+ DATA = b'x' * 2000000
18
+ def note(**kw):
19
+ with open(LOG, 'a') as f: f.write(json.dumps(kw) + '\n')
20
+ class H(BaseHTTPRequestHandler):
21
+ def send(self, code, obj):
22
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(obj).encode())
23
+ def do_GET(self):
24
+ if self.path.startswith('/store/'):
25
+ data = DATA[:1000] if 'short' in self.path else DATA
26
+ note(op='download', range=self.headers.get('Range'))
27
+ self.send_response(200); self.send_header('Content-Length', str(len(data))); self.end_headers(); self.wfile.write(data); return
28
+ hid = self.path.rsplit('/', 1)[-1]
29
+ if hid == PEND:
30
+ return self.send(200, {'success': True, 'data': {'handoffId': hid, 'status': 'pending', 'sizeBytes': 5, 'fileName': 'a'}})
31
+ if hid in (OK, SHORT):
32
+ return self.send(200, {'success': True, 'data': {'handoffId': hid, 'status': 'ready', 'sizeBytes': len(DATA), 'fileName': '../../evil name.mp4', 'note': 'today',
33
+ 'download': {'url': f'http://127.0.0.1:{PORT}/store/' + ('short' if hid == SHORT else 'ok'), 'expiresAt': 'x'}}})
34
+ self.send(404, {'success': False, 'error': 'not_found', 'message': 'This handoff is gone.'})
35
+ def do_POST(self):
36
+ note(op='ack' if self.path.endswith('/ack') else 'other', path=self.path, session=self.headers.get('X-Agent-Session')); self.send(200, {'success': True, 'data': {'deleted': True}})
37
+ def log_message(self, *a, **k): pass
38
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
39
+ PY
40
+ STUB_PID=$!
41
+ disown "$STUB_PID" 2>/dev/null || true
42
+ WORK="$(cd "$(mktemp -d)" && pwd -P)"
43
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -rf "$STUB_LOG" "$WORK"' EXIT
44
+ 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
45
+ mkdir -p "$WORK/home/.crewly" "$WORK/proj"
46
+ ENVS=(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=dev-mia HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly" CREWLY_PROJECT_PATH="$WORK/proj")
47
+ run() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>/dev/null; }
48
+ run_err() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
49
+
50
+ OK=hf_AbCdEfGhIjKlMnOpQrStUv
51
+ OUT=$(run --id $OK)
52
+ DEST="$WORK/proj/received/evil name.mp4"
53
+ check "result" "$(printf '%s' "$OUT" | jq -c '{success, fileName, sizeBytes, acked, note}')" '{"success":true,"fileName":"evil name.mp4","sizeBytes":2000000,"acked":true,"note":"today"}'
54
+ check "saved in the default folder under a safe name" "$(wc -c < "$DEST" | tr -d ' ')" "2000000"
55
+ check "ack sent as this agent" "$(grep '"op": "ack"' "$STUB_LOG" | jq -c '{path, session}')" '{"path":"/api/apps/handoffs/hf_AbCdEfGhIjKlMnOpQrStUv/ack","session":"dev-mia"}'
56
+ check "no .part left" "$(ls -a "$WORK/proj/received" | grep -c part)" "0"
57
+
58
+ OUT=$(run --id $OK --dir "$WORK/proj/inbox")
59
+ check "second copy goes to --dir" "$(printf '%s' "$OUT" | jq -r .path)" "$WORK/proj/inbox/evil name.mp4"
60
+ run --id $OK >/dev/null
61
+ check "never overwrites" "$(ls "$WORK/proj/received" | sort | tr '\n' '|')" "evil name-1.mp4|evil name.mp4|"
62
+
63
+ : > "$STUB_LOG"
64
+ OUT=$(run --id $OK --no-ack --dir "$WORK/proj/noack")
65
+ check "--no-ack keeps the server copy" "$(printf '%s' "$OUT" | jq -c '{acked, warning: (.warning != null)}')" '{"acked":false,"warning":true}'
66
+ check "no ack request" "$(grep -c '"op": "ack"' "$STUB_LOG")" "0"
67
+
68
+ : > "$STUB_LOG"
69
+ OUT=$(run --id hf_ShortShortShortShortSh --dir "$WORK/proj/short"; true)
70
+ check "size mismatch fails, removes the partial and does not ack" "$(printf '%s' "$OUT" | jq -r .reason)|$(ls -A "$WORK/proj/short" | wc -l | tr -d ' ')|$(grep -c '"op": "ack"' "$STUB_LOG")" "size_mismatch|0|0"
71
+
72
+ OUT=$(run --id hf_PendPendPendPendPendPe; true)
73
+ check "pending handoff is not ready" "$(printf '%s' "$OUT" | jq -r .reason)" "not_ready"
74
+ OUT=$(run --id hf_GoneGoneGoneGoneGoneGo; true)
75
+ check "unknown handoff passes Cloud's reason" "$(printf '%s' "$OUT" | jq -r .reason)" "not_found"
76
+ check "bad id refused" "$(run_err --id nope | grep -c 'handoff id')" "1"
77
+ check "dir inside Crewly home refused" "$(run_err --id $OK --dir "$WORK/home/.crewly/x" | grep -c 'Crewly home')" "1"
78
+
79
+ echo "receive-file: $PASS passed, $FAIL failed"
80
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: Send File
3
+ description: Hand a LARGE file (video, archive, dataset, up to 2 GB) to another agent of the same Crewly account, on this or another machine, through a temporary Crewly Cloud relay. Prefer this over Drive or Slack for anything bigger than a few MB. The receiver deletes it with receive-file; unclaimed files are deleted after 24 hours.
4
+ version: 1.0.0
5
+ category: productivity
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - send file
11
+ - send a large file
12
+ - hand off a file
13
+ - pass the video to
14
+ - transfer file to agent
15
+ - share a big file
16
+ tags:
17
+ - files
18
+ - handoff
19
+ - transfer
20
+ execution:
21
+ type: script
22
+ script:
23
+ file: execute.sh
24
+ interpreter: bash
25
+ timeoutMs: 3600000
26
+ ---
27
+
28
+ # Send File
29
+
30
+ Passes one big file to another agent of **your own Crewly account**
31
+ (same machine or another one) without Slack or Drive. Needs the machine to be
32
+ signed in to Crewly Cloud. The file is streamed from disk (never loaded into
33
+ memory), kept on a temporary server, and deleted as soon as the receiver
34
+ acknowledges it, or after 24 hours.
35
+
36
+ ```bash
37
+ bash execute.sh --path ./out/daily-2026-10-09.mp4 --note "today's cut, 300 MB" --to Mia
38
+ ```
39
+
40
+ Prints:
41
+
42
+ ```json
43
+ {"success":true,"handoffId":"hf_…","fileName":"daily-2026-10-09.mp4","sizeBytes":314572800,"expiresAt":"…",
44
+ "instruction":"Receive it with the receive-file skill: bash <skills>/core/receive-file/execute.sh --id hf_… [--dir <folder>]"}
45
+ ```
46
+
47
+ **Send the `instruction` line to the receiving agent** (use your normal
48
+ agent-to-agent message), and tell it the handoff id. Do not post the id anywhere
49
+ public.
50
+
51
+ Options:
52
+
53
+ - `--path` (required): a regular file inside your project directory (not a
54
+ symlink, not inside Crewly's home). Max 2 GB; the account may hold 5 GB of
55
+ waiting handoffs at once, so wait for receivers to finish before sending more.
56
+ - `--name`: the name the receiver sees (default: the file's name).
57
+ - `--note`: one line for the receiver. `--to`: who it is for (informational).
58
+ - `--content-type`: default from the extension.
59
+
60
+ Not for secrets or credentials. Not for files under ~5 MB that can go in a
61
+ message. Cancel a handoff you no longer want received with
62
+ `bash execute.sh --cancel hf_…`.
@@ -0,0 +1,145 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # send-file — hand a large file to another agent of the same Crewly account
4
+ # through the Cloud file-handoff relay (crewly-services apps/SPEC.md section 18).
5
+ #
6
+ # The backend (POST /api/apps/handoffs) asks Cloud for a presigned upload URL;
7
+ # this script then streams the file straight to object storage with curl -T
8
+ # (from disk, never buffered), and confirms with /complete.
9
+ #
10
+ # Usage:
11
+ # bash execute.sh --path <file> [--name <name>] [--note <text>] [--to <agent>] [--content-type <type>]
12
+ # bash execute.sh --cancel <handoffId>
13
+ # =============================================================================
14
+ set -euo pipefail
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
17
+
18
+ print_usage() {
19
+ cat <<'EOF_USAGE'
20
+ Usage:
21
+ bash execute.sh --path <file> [--name <name>] [--note <text>] [--to <agent>] [--content-type <type>]
22
+ bash execute.sh --cancel <handoffId>
23
+
24
+ Options:
25
+ --path Regular file inside your project directory, up to 2 GB (required)
26
+ --name Name the receiver sees (default: the file's name)
27
+ --note One line for the receiver
28
+ --to Who it is for (informational; any agent of the account can receive it)
29
+ --content-type MIME type (default: from the extension)
30
+ --cancel Withdraw a handoff you made (deletes the uploaded file)
31
+ --help | -h Show this help
32
+ EOF_USAGE
33
+ }
34
+
35
+ fail_from() {
36
+ printf '%s' "$1" | jq -c '{success: false, status: (.status // 0), reason: (.details.error // .error // "unknown"), message: (.details.message // .details // "")}' 2>/dev/null \
37
+ || jq -cn --arg r "$1" '{success: false, reason: $r}'
38
+ exit 1
39
+ }
40
+
41
+ call() {
42
+ local out
43
+ out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
44
+ printf '%s\n' "$out" | tail -n 1
45
+ }
46
+
47
+ content_type_for() {
48
+ case "$(printf '%s' "${1##*.}" | tr '[:upper:]' '[:lower:]')" in
49
+ mp4|m4v) echo video/mp4 ;;
50
+ mov) echo video/quicktime ;;
51
+ webm) echo video/webm ;;
52
+ mkv) echo video/x-matroska ;;
53
+ mp3) echo audio/mpeg ;;
54
+ wav) echo audio/wav ;;
55
+ m4a) echo audio/mp4 ;;
56
+ png) echo image/png ;;
57
+ jpg|jpeg) echo image/jpeg ;;
58
+ pdf) echo application/pdf ;;
59
+ zip) echo application/zip ;;
60
+ json) echo application/json ;;
61
+ csv) echo text/csv ;;
62
+ txt|md|log) echo text/plain ;;
63
+ *) echo application/octet-stream ;;
64
+ esac
65
+ }
66
+
67
+ # Regular, non-symlink file inside the project directory, never under Crewly's
68
+ # home. Prints the real path, then the size in bytes.
69
+ check_file() {
70
+ command -v node >/dev/null 2>&1 || error_exit "node is required"
71
+ CF_PATH="$1" CF_PROJECT="${CREWLY_PROJECT_PATH:-$PWD}" node <<'NODE'
72
+ const fs = require('fs'); const path = require('path'); const os = require('os');
73
+ const fail = (m) => { process.stderr.write(JSON.stringify({ error: m }) + '\n'); process.exit(1); };
74
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
75
+ const within = (c, p) => c === p || c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
76
+ const given = process.env.CF_PATH;
77
+ const abs = path.resolve(given);
78
+ let st;
79
+ try { st = fs.lstatSync(abs); } catch { fail(`file not found: ${given}`); }
80
+ if (st.isSymbolicLink()) fail(`${given} is a symbolic link; pass the real file`);
81
+ if (!st.isFile()) fail(`${given} is not a regular file`);
82
+ if (st.size === 0) fail(`${given} is empty`);
83
+ const r = real(abs); const project = real(process.env.CF_PROJECT || process.cwd());
84
+ if (!project) fail('cannot resolve the project directory');
85
+ const homes = [path.join(os.homedir(), '.crewly'), process.env.CREWLY_HOME].filter(Boolean).map((p) => real(p) || path.resolve(p));
86
+ if (homes.some((h) => within(r, h))) fail(`${given} is inside Crewly's home directory`);
87
+ if (!within(r, project)) fail(`${given} is outside your project directory (${project}); copy it there first`);
88
+ process.stdout.write(r + '\n' + st.size + '\n');
89
+ NODE
90
+ }
91
+
92
+ FILE=""; NAME=""; NOTE=""; TO=""; CTYPE=""; CANCEL=""
93
+ while [[ $# -gt 0 ]]; do
94
+ case "$1" in
95
+ --path) [ $# -ge 2 ] || error_exit "--path requires a value"; FILE="$2"; shift 2 ;;
96
+ --name) [ $# -ge 2 ] || error_exit "--name requires a value"; NAME="$2"; shift 2 ;;
97
+ --note) [ $# -ge 2 ] || error_exit "--note requires a value"; NOTE="$2"; shift 2 ;;
98
+ --to) [ $# -ge 2 ] || error_exit "--to requires a value"; TO="$2"; shift 2 ;;
99
+ --content-type) [ $# -ge 2 ] || error_exit "--content-type requires a value"; CTYPE="$2"; shift 2 ;;
100
+ --cancel) [ $# -ge 2 ] || error_exit "--cancel requires a handoff id"; CANCEL="$2"; shift 2 ;;
101
+ --help|-h) print_usage; exit 0 ;;
102
+ *) error_exit "Unknown option: $1" ;;
103
+ esac
104
+ done
105
+
106
+ if [ -n "$CANCEL" ]; then
107
+ [[ "$CANCEL" =~ ^hf_[A-Za-z0-9_-]{22}$ ]] || error_exit "--cancel needs a handoff id like hf_…"
108
+ RESULT=$(call DELETE "/apps/handoffs/${CANCEL}") || { printf '%s\n' "$RESULT"; exit 1; }
109
+ jq -cn --arg id "$CANCEL" '{success: true, handoffId: $id, cancelled: true}'
110
+ exit 0
111
+ fi
112
+
113
+ [ -n "$FILE" ] || error_exit "--path is required"
114
+ INFO=$(check_file "$FILE") || { printf '%s\n' "$INFO" >&2; exit 1; }
115
+ REAL=$(printf '%s\n' "$INFO" | sed -n 1p)
116
+ SIZE=$(printf '%s\n' "$INFO" | sed -n 2p)
117
+ [ -n "$NAME" ] || NAME="$(basename "$REAL")"
118
+ [ -n "$CTYPE" ] || CTYPE="$(content_type_for "$REAL")"
119
+
120
+ BODY=$(jq -cn --arg n "$NAME" --argjson s "$SIZE" --arg c "$CTYPE" --arg note "$NOTE" --arg to "$TO" \
121
+ '{fileName: $n, sizeBytes: $s, contentType: $c} + (if $note != "" then {note: $note} else {} end) + (if $to != "" then {to: $to} else {} end)')
122
+ CREATED=$(call POST "/apps/handoffs" "$BODY") || { printf '%s\n' "$CREATED"; exit 1; }
123
+ ID=$(printf '%s' "$CREATED" | jq -r '.data.handoffId // empty')
124
+ URL=$(printf '%s' "$CREATED" | jq -r '.data.upload.url // empty')
125
+ [ -n "$ID" ] && [ -n "$URL" ] || error_exit "Cloud answered without an upload URL"
126
+
127
+ # Headers the presigned URL was signed with; the file streams from disk (-T).
128
+ HDRS=()
129
+ while IFS= read -r line; do HDRS+=(-H "$line"); done < <(printf '%s' "$CREATED" | jq -r '.data.upload.headers // {} | to_entries[] | "\(.key): \(.value)"')
130
+
131
+ cancel_quietly() { api_call DELETE "/apps/handoffs/${ID}" >/dev/null 2>&1 || true; }
132
+
133
+ CODE=$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 30 --retry 3 --retry-delay 3 -X PUT -T "$REAL" ${HDRS[@]+"${HDRS[@]}"} "$URL" 2>/dev/null) || CODE="000"
134
+ if [ "$CODE" != "200" ]; then
135
+ cancel_quietly
136
+ jq -cn --arg c "$CODE" '{success: false, reason: "upload_failed", message: ("The upload to temporary storage failed (HTTP " + $c + "). Nothing was kept; try again.")}'
137
+ exit 1
138
+ fi
139
+
140
+ DONE=$(api_call POST "/apps/handoffs/${ID}/complete" 2>&1) || { cancel_quietly; fail_from "$(printf '%s\n' "$DONE" | tail -n 1)"; }
141
+ EXPIRES=$(printf '%s\n' "$DONE" | tail -n 1 | jq -r '.data.expiresAt // empty')
142
+
143
+ jq -cn --arg id "$ID" --arg n "$NAME" --argjson s "$SIZE" --arg e "$EXPIRES" \
144
+ '{success: true, handoffId: $id, fileName: $n, sizeBytes: $s, expiresAt: $e,
145
+ instruction: ("Receive it with the receive-file skill: bash <skills>/core/receive-file/execute.sh --id " + $id + " [--dir <folder>]")}'
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Tests for send-file — run with: bash execute.test.sh
3
+ # A Python stub plays both the Crewly backend and the object storage.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() { if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi; }
9
+
10
+ PORT=18861
11
+ STUB_LOG="$(mktemp)"; STORE="$(mktemp)"; export STUB_LOG STORE STUB_PORT="$PORT"
12
+ python3 -u <<'PY' >/dev/null 2>&1 &
13
+ from http.server import BaseHTTPRequestHandler, HTTPServer
14
+ import json, os
15
+ LOG = os.environ['STUB_LOG']; STORE = os.environ['STORE']; PORT = int(os.environ['STUB_PORT'])
16
+ ID = 'hf_AbCdEfGhIjKlMnOpQrStUv'
17
+ def note(**kw):
18
+ with open(LOG, 'a') as f: f.write(json.dumps(kw) + '\n')
19
+ class H(BaseHTTPRequestHandler):
20
+ def send(self, code, obj):
21
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(json.dumps(obj).encode())
22
+ def body(self):
23
+ n = int(self.headers.get('Content-Length') or 0); return self.rfile.read(n)
24
+ def do_POST(self):
25
+ b = self.body()
26
+ if self.path == '/api/apps/handoffs':
27
+ req = json.loads(b); note(op='create', req=req, session=self.headers.get('X-Agent-Session'))
28
+ if req['fileName'] == 'refuse.bin':
29
+ return self.send(402, {'success': False, 'error': 'quota_exceeded', 'message': 'Too many bytes waiting.'})
30
+ return self.send(201, {'success': True, 'data': {'handoffId': ID, 'status': 'pending', 'upload': {'url': f'http://127.0.0.1:{PORT}/store/obj', 'method': 'PUT', 'headers': {'Content-Type': req['contentType'], 'x-amz-server-side-encryption': 'AES256'}}}})
31
+ if self.path == f'/api/apps/handoffs/{ID}/complete':
32
+ note(op='complete'); return self.send(200, {'success': True, 'data': {'handoffId': ID, 'status': 'ready', 'expiresAt': '2026-10-10T00:00:00.000Z'}})
33
+ self.send(404, {'success': False})
34
+ def do_DELETE(self):
35
+ note(op='cancel', path=self.path); self.send(200, {'success': True, 'data': {'deleted': True}})
36
+ def do_PUT(self):
37
+ b = self.body()
38
+ note(op='put', bytes=len(b), ctype=self.headers.get('Content-Type'), sse=self.headers.get('x-amz-server-side-encryption'))
39
+ self.send_response(500 if os.path.exists(STORE + '.fail') else 200); self.end_headers()
40
+ def log_message(self, *a, **k): pass
41
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
42
+ PY
43
+ STUB_PID=$!
44
+ disown "$STUB_PID" 2>/dev/null || true
45
+ WORK="$(mktemp -d)"
46
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -rf "$STUB_LOG" "$STORE" "$STORE.fail" "$WORK"' EXIT
47
+ 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
48
+ mkdir -p "$WORK/home/.crewly" "$WORK/proj"
49
+ ENVS=(CREWLY_API_URL="http://127.0.0.1:${PORT}" CREWLY_SESSION_NAME=dev-ella HOME="$WORK/home" CREWLY_HOME="$WORK/home/.crewly" CREWLY_PROJECT_PATH="$WORK/proj")
50
+ run() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>/dev/null; }
51
+ run_err() { env "${ENVS[@]}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
52
+
53
+ head -c 3000000 /dev/zero > "$WORK/proj/daily.mp4"
54
+ OUT=$(run --path "$WORK/proj/daily.mp4" --note "today" --to Mia)
55
+ check "result" "$(printf '%s' "$OUT" | jq -c '{success, handoffId, fileName, sizeBytes, expiresAt}')" '{"success":true,"handoffId":"hf_AbCdEfGhIjKlMnOpQrStUv","fileName":"daily.mp4","sizeBytes":3000000,"expiresAt":"2026-10-10T00:00:00.000Z"}'
56
+ check "instruction names the receiver skill" "$(printf '%s' "$OUT" | jq -r '.instruction' | grep -c 'receive-file/execute.sh --id hf_AbCdEfGhIjKlMnOpQrStUv')" "1"
57
+ check "create request" "$(sed -n 1p "$STUB_LOG" | jq -c '{req, session}')" '{"req":{"fileName":"daily.mp4","sizeBytes":3000000,"contentType":"video/mp4","note":"today","to":"Mia"},"session":"dev-ella"}'
58
+ check "upload streamed with signed headers" "$(sed -n 2p "$STUB_LOG" | jq -c '{bytes, ctype, sse}')" '{"bytes":3000000,"ctype":"video/mp4","sse":"AES256"}'
59
+ check "complete called" "$(sed -n 3p "$STUB_LOG" | jq -r .op)" "complete"
60
+
61
+ : > "$STUB_LOG"
62
+ OUT=$(run --path "$WORK/proj/daily.mp4" --name "refuse.bin"; true)
63
+ check "cloud refusal passed through" "$(printf '%s' "$OUT" | jq -c '{success, reason}')" '{"success":false,"reason":"quota_exceeded"}'
64
+
65
+ : > "$STUB_LOG"; touch "$STORE.fail"
66
+ OUT=$(run --path "$WORK/proj/daily.mp4"; true)
67
+ check "failed upload reports and cancels" "$(printf '%s' "$OUT" | jq -r .reason)" "upload_failed"
68
+ check "cancel issued" "$(grep -c '"op": "cancel"' "$STUB_LOG")" "1"
69
+ rm -f "$STORE.fail"
70
+
71
+ echo secret > "$WORK/outside.txt"; ln -s "$WORK/proj/daily.mp4" "$WORK/proj/link.mp4"
72
+ check "outside project refused" "$(run_err --path "$WORK/outside.txt" | grep -c 'outside your project directory')" "1"
73
+ check "symlink refused" "$(run_err --path "$WORK/proj/link.mp4" | grep -c 'symbolic link')" "1"
74
+ check "missing file refused" "$(run_err --path "$WORK/proj/nope" | grep -c 'file not found')" "1"
75
+ check "path required" "$(run_err | grep -c -- '--path is required')" "1"
76
+ : > "$STUB_LOG"
77
+ run --cancel hf_AbCdEfGhIjKlMnOpQrStUv >/dev/null
78
+ check "cancel" "$(jq -r .path "$STUB_LOG")" "/api/apps/handoffs/hf_AbCdEfGhIjKlMnOpQrStUv"
79
+
80
+ echo "send-file: $PASS passed, $FAIL failed"
81
+ [ "$FAIL" -eq 0 ]
@@ -1663,6 +1663,23 @@ export declare const ACTIVITY_MONITOR_CONSTANTS: {
1663
1663
  * (spinners, TUI re-renders). Default: 15 minutes. */
1664
1664
  readonly MAX_IN_PROGRESS_MS: 900000;
1665
1665
  };
1666
+ /**
1667
+ * Antigravity "Running command..." hang (2026-10-10): agy's TUI kept a
1668
+ * "Running command..." spinner for 8+ minutes after the command had finished
1669
+ * and agy had no child process. The agent stayed busy forever, so queued
1670
+ * messages and the runtime switch (which waits for a safe point) never ran.
1671
+ * Escape un-sticks it.
1672
+ */
1673
+ export declare const ANTIGRAVITY_STUCK_COMMAND_CONSTANTS: {
1674
+ /** The spinner must have been on screen this long (ms) before Escape is sent */
1675
+ readonly STUCK_AFTER_MS: number;
1676
+ /** Not more than one Escape per session in this window (ms) */
1677
+ readonly REPEAT_COOLDOWN_MS: number;
1678
+ /** Bottom screen lines searched for the spinner */
1679
+ readonly SCREEN_LINES: 20;
1680
+ /** The spinner label (case-insensitive) */
1681
+ readonly SPINNER_PATTERN: RegExp;
1682
+ };
1666
1683
  /**
1667
1684
  * Give-up recovery (#841, specs/2026-09-27-give-up-recovery.md): when a worker
1668
1685
  * stops on a feasibility give-up, queue a retry with a different approach,