crewly 1.20.265 → 1.20.267

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 (125) hide show
  1. package/config/skills/_common/lib.sh +15 -2
  2. package/config/skills/agent/core/app-data/SKILL.md +13 -1
  3. package/config/skills/agent/core/app-data/execute.sh +41 -5
  4. package/config/skills/agent/core/app-data/execute.test.sh +47 -1
  5. package/config/skills/agent/core/publish-app/SKILL.md +1 -1
  6. package/config/skills/agent/core/slack-file/SKILL.md +53 -0
  7. package/config/skills/agent/core/slack-file/execute.sh +72 -0
  8. package/config/skills/agent/core/slack-file/execute.test.sh +34 -0
  9. package/dist/backend/backend/src/constants.d.ts +59 -0
  10. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  11. package/dist/backend/backend/src/constants.js +59 -0
  12. package/dist/backend/backend/src/constants.js.map +1 -1
  13. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +7 -0
  14. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  15. package/dist/backend/backend/src/controllers/apps/apps.controller.js +23 -0
  16. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +9 -0
  18. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/apps/apps.routes.js +19 -1
  20. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/connector/connector.routes.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/connector/connector.routes.js +6 -1
  23. package/dist/backend/backend/src/controllers/connector/connector.routes.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts +69 -0
  25. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts.map +1 -0
  26. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js +203 -0
  27. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js.map +1 -0
  28. package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.d.ts +16 -0
  29. package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.d.ts.map +1 -1
  30. package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.js +126 -5
  31. package/dist/backend/backend/src/controllers/connector/remote-mcp.controller.js.map +1 -1
  32. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  33. package/dist/backend/backend/src/controllers/slack/slack.controller.js +37 -0
  34. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  35. package/dist/backend/backend/src/index.d.ts.map +1 -1
  36. package/dist/backend/backend/src/index.js +5 -0
  37. package/dist/backend/backend/src/index.js.map +1 -1
  38. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  39. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +2 -1
  40. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  41. package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts +2 -0
  42. package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts.map +1 -1
  43. package/dist/backend/backend/src/services/apps/apps-cloud.client.js +1 -0
  44. package/dist/backend/backend/src/services/apps/apps-cloud.client.js.map +1 -1
  45. package/dist/backend/backend/src/services/apps/apps.service.d.ts +17 -0
  46. package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/apps/apps.service.js +24 -0
  48. package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
  49. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +4 -3
  51. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.d.ts +43 -0
  53. package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.d.ts.map +1 -0
  54. package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.js +98 -0
  55. package/dist/backend/backend/src/services/connector/remote-mcp-auth-card.js.map +1 -0
  56. package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.d.ts +277 -0
  57. package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.d.ts.map +1 -0
  58. package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.js +719 -0
  59. package/dist/backend/backend/src/services/connector/remote-mcp-auth.service.js.map +1 -0
  60. package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.d.ts +29 -5
  61. package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.js +40 -12
  63. package/dist/backend/backend/src/services/connector/remote-mcp-launch.service.js.map +1 -1
  64. package/dist/backend/backend/src/services/connector/remote-mcp-oauth.d.ts +196 -0
  65. package/dist/backend/backend/src/services/connector/remote-mcp-oauth.d.ts.map +1 -0
  66. package/dist/backend/backend/src/services/connector/remote-mcp-oauth.js +351 -0
  67. package/dist/backend/backend/src/services/connector/remote-mcp-oauth.js.map +1 -0
  68. package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.d.ts +3 -1
  69. package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.d.ts.map +1 -1
  70. package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.js +8 -0
  71. package/dist/backend/backend/src/services/connector/remote-mcp-probe.service.js.map +1 -1
  72. package/dist/backend/backend/src/services/core/credential-files.d.ts.map +1 -1
  73. package/dist/backend/backend/src/services/core/credential-files.js +2 -0
  74. package/dist/backend/backend/src/services/core/credential-files.js.map +1 -1
  75. package/dist/backend/backend/src/services/slack/slack-file-fetch.service.d.ts +119 -0
  76. package/dist/backend/backend/src/services/slack/slack-file-fetch.service.d.ts.map +1 -0
  77. package/dist/backend/backend/src/services/slack/slack-file-fetch.service.js +415 -0
  78. package/dist/backend/backend/src/services/slack/slack-file-fetch.service.js.map +1 -0
  79. package/dist/backend/backend/src/services/slack/slack-file-ref.d.ts +69 -0
  80. package/dist/backend/backend/src/services/slack/slack-file-ref.d.ts.map +1 -0
  81. package/dist/backend/backend/src/services/slack/slack-file-ref.js +177 -0
  82. package/dist/backend/backend/src/services/slack/slack-file-ref.js.map +1 -0
  83. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  84. package/dist/backend/backend/src/services/slack/slack-initializer.js +16 -0
  85. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  86. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +15 -0
  87. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  88. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +84 -1
  89. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  90. package/dist/backend/backend/src/types/slack.types.d.ts +11 -0
  91. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  92. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  93. package/dist/backend/build-info.json +2 -2
  94. package/dist/cli/backend/src/constants.d.ts +59 -0
  95. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  96. package/dist/cli/backend/src/constants.js +59 -0
  97. package/dist/cli/backend/src/constants.js.map +1 -1
  98. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  99. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +2 -1
  100. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  101. package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.d.ts +29 -5
  102. package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.d.ts.map +1 -1
  103. package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.js +40 -12
  104. package/dist/cli/backend/src/services/connector/remote-mcp-launch.service.js.map +1 -1
  105. package/dist/cli/backend/src/services/core/credential-files.d.ts.map +1 -1
  106. package/dist/cli/backend/src/services/core/credential-files.js +2 -0
  107. package/dist/cli/backend/src/services/core/credential-files.js.map +1 -1
  108. package/dist/cli/backend/src/services/slack/slack-file-fetch.service.d.ts +119 -0
  109. package/dist/cli/backend/src/services/slack/slack-file-fetch.service.d.ts.map +1 -0
  110. package/dist/cli/backend/src/services/slack/slack-file-fetch.service.js +415 -0
  111. package/dist/cli/backend/src/services/slack/slack-file-fetch.service.js.map +1 -0
  112. package/dist/cli/backend/src/services/slack/slack-file-ref.d.ts +69 -0
  113. package/dist/cli/backend/src/services/slack/slack-file-ref.d.ts.map +1 -0
  114. package/dist/cli/backend/src/services/slack/slack-file-ref.js +177 -0
  115. package/dist/cli/backend/src/services/slack/slack-file-ref.js.map +1 -0
  116. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +15 -0
  117. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  118. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +84 -1
  119. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  120. package/dist/cli/backend/src/types/slack.types.d.ts +11 -0
  121. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  122. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  123. package/frontend/dist/assets/{index-83188f0b.js → index-7e5ebf78.js} +111 -111
  124. package/frontend/dist/index.html +1 -1
  125. package/package.json +1 -1
@@ -221,7 +221,16 @@ api_call() {
221
221
  local method="$1" endpoint="$2" body="${3:-}"
222
222
  local url="${CREWLY_API_URL}/api${endpoint}"
223
223
  _prune_skill_output
224
- local args=(-s -w '\n%{http_code}' -X "$method" -H "Content-Type: application/json")
224
+ local args=(-s -w '\n%{http_code}' -X "$method")
225
+ # A raw file upload (app-data --upload): CREWLY_API_UPLOAD_FILE is sent as the
226
+ # body, byte for byte, with CREWLY_API_UPLOAD_TYPE as its Content-Type and the
227
+ # URI-encoded CREWLY_API_UPLOAD_NAME as X-File-Name. Everything else is JSON.
228
+ if [ -n "${CREWLY_API_UPLOAD_FILE:-}" ]; then
229
+ args+=(-H "Content-Type: ${CREWLY_API_UPLOAD_TYPE:-application/octet-stream}")
230
+ [ -n "${CREWLY_API_UPLOAD_NAME:-}" ] && args+=(-H "X-File-Name: ${CREWLY_API_UPLOAD_NAME}")
231
+ else
232
+ args+=(-H "Content-Type: application/json")
233
+ fi
225
234
  # Include agent session identity header for heartbeat tracking
226
235
  # Use ${VAR:-} pattern to avoid 'unbound variable' error under set -u (nounset)
227
236
  # The agent badge (#999): the credential that makes the backend treat this
@@ -280,7 +289,11 @@ api_call() {
280
289
  if [ -n "${CREWLY_AGENT_GOAL:-}" ]; then
281
290
  args+=(-H "X-Agent-Goal: b64:$(printf '%s' "$CREWLY_AGENT_GOAL" | base64 | tr -d '\n')")
282
291
  fi
283
- [ -n "$body" ] && args+=(-d "$body")
292
+ if [ -n "${CREWLY_API_UPLOAD_FILE:-}" ]; then
293
+ args+=(--data-binary "@${CREWLY_API_UPLOAD_FILE}")
294
+ elif [ -n "$body" ]; then
295
+ args+=(-d "$body")
296
+ fi
284
297
 
285
298
  local response
286
299
  response=$(curl "${args[@]}" "$url")
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: App Data
3
3
  description: Read and write the data of a Crewly App you published (the same collections the app's page sees through crewly.db) — list, get, set, update, add, delete. Use it to fill an app with content, act on what the owner entered, or answer an [APP CHANGES] message.
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  category: productivity
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -64,6 +64,18 @@ Output:
64
64
  otherwise).
65
65
  - `--data-file <path>` reads the JSON from a regular file inside your project
66
66
  directory (not a symlink, not under `~/.crewly`, at most 1 MB).
67
+ - `--upload <path> [--name <file name>]` puts a file (an image for a post) into
68
+ the app and prints `{ fileId, name, size, contentType, url }`. Same rules as
69
+ `--data-file` (regular file inside your project directory, not a symlink, not
70
+ under `~/.crewly`), up to 10 MB; the app's own limit may be smaller and its
71
+ refusal comes back as-is (`too_large`, quota). It works for the app's owner
72
+ agents and for collaborators; anyone else gets `not_your_app`. Put the `url`
73
+ into a document the page renders (for example `images[].src`) with
74
+ `--update`; PNG, JPEG, GIF and WebP show inline, other types download.
75
+ ```bash
76
+ bash execute.sh --app <appId> --upload ./cover.png
77
+ bash execute.sh --app <appId> --update posts <docId> --data '{"images":[{"src":"<url from the upload>"}]}'
78
+ ```
67
79
  - `--list` pages with `--after <next>` until `next` is null.
68
80
  - `--update` is a shallow merge; `--set` replaces the whole document.
69
81
  - `--if-rev` makes an update conditional: if the owner changed the doc since
@@ -19,6 +19,7 @@ Usage:
19
19
  bash execute.sh --app <appId> --update <collection> <docId> --data '{"done":true}' [--if-rev 4]
20
20
  bash execute.sh --app <appId> --add <collection> --data '{"name":"milk"}'
21
21
  bash execute.sh --app <appId> --delete <collection> <docId>
22
+ bash execute.sh --app <appId> --upload <path-to-image> [--name <file name>]
22
23
  bash execute.sh --app <appId> --request-access [--scope team|agent] [--reason "why"]
23
24
  bash execute.sh --app <appId> --collaborators
24
25
  bash execute.sh --app <appId> --owner
@@ -31,6 +32,8 @@ Options:
31
32
  --if-rev Only update when the doc is still at this rev (409 conflict otherwise)
32
33
  --limit Page size for --list (1-500, default 100)
33
34
  --after Continue a --list after this doc id (the previous page's "next")
35
+ --upload Put a file (an image for a post) into the app and print its url; works for the owner's agents and for collaborators. Regular, non-symlink file inside your project directory, up to 10 MB (the app's own limit may be smaller)
36
+ --name File name to show for --upload (default: the file's name)
34
37
  --request-access Ask the owner to let your team (default) or only you work in an app another team published; a card goes to the owner, nothing is granted until they tap Allow
35
38
  --collaborators Who the owner let work in the app
36
39
  --scope team (your team, default) or agent (only you), with --request-access
@@ -60,7 +63,7 @@ uri() { jq -rn --arg v "$1" '$v|@uri'; }
60
63
  # (CREWLY_PROJECT_PATH, else the working directory), never under Crewly's home.
61
64
  read_data_file() {
62
65
  command -v node >/dev/null 2>&1 || error_exit "node is required for --data-file"
63
- DF_PATH="$1" DF_PROJECT="${CREWLY_PROJECT_PATH:-$PWD}" node <<'NODE'
66
+ DF_MODE="${2:-read}" DF_PATH="$1" DF_PROJECT="${CREWLY_PROJECT_PATH:-$PWD}" node <<'NODE'
64
67
  const fs = require('fs'); const path = require('path'); const os = require('os');
65
68
  const fail = (m) => { process.stderr.write(JSON.stringify({ error: m }) + '\n'); process.exit(1); };
66
69
  const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
@@ -76,12 +79,35 @@ if (!project) fail('cannot resolve the project directory');
76
79
  const homes = [path.join(os.homedir(), '.crewly'), process.env.CREWLY_HOME].filter(Boolean).map((p) => real(p) || path.resolve(p));
77
80
  if (homes.some((h) => within(r, h))) fail(`${given} is inside Crewly's home directory`);
78
81
  if (!within(r, project)) fail(`${given} is outside your project directory (${project})`);
79
- if (st.size > 1024 * 1024) fail(`${given} is larger than 1 MB`);
80
- process.stdout.write(fs.readFileSync(r, 'utf8'));
82
+ if (process.env.DF_MODE === 'path') {
83
+ if (st.size === 0) fail(`${given} is empty`);
84
+ if (st.size > 10 * 1024 * 1024) fail(`${given} is larger than 10 MB`);
85
+ process.stdout.write(r);
86
+ } else {
87
+ if (st.size > 1024 * 1024) fail(`${given} is larger than 1 MB`);
88
+ process.stdout.write(fs.readFileSync(r, 'utf8'));
89
+ }
81
90
  NODE
82
91
  }
83
92
 
84
- APP=""; OP=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""; SCOPE=""; REASON=""; SET_OWNER=""; WHO=""
93
+ # Content type from the extension. JSON-like types are sent as octet-stream so
94
+ # the backend receives raw bytes; Cloud decides what a browser may show inline.
95
+ content_type_for() {
96
+ case "$(printf '%s' "${1##*.}" | tr '[:upper:]' '[:lower:]')" in
97
+ png) echo image/png ;;
98
+ jpg|jpeg) echo image/jpeg ;;
99
+ gif) echo image/gif ;;
100
+ webp) echo image/webp ;;
101
+ svg) echo image/svg+xml ;;
102
+ pdf) echo application/pdf ;;
103
+ mp4) echo video/mp4 ;;
104
+ webm) echo video/webm ;;
105
+ txt) echo text/plain ;;
106
+ *) echo application/octet-stream ;;
107
+ esac
108
+ }
109
+
110
+ APP=""; OP=""; UPLOAD=""; NAME=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""; SCOPE=""; REASON=""; SET_OWNER=""; WHO=""
85
111
  while [[ $# -gt 0 ]]; do
86
112
  case "$1" in
87
113
  --app) [ $# -ge 2 ] || error_exit "--app requires a value"; APP="$2"; shift 2 ;;
@@ -96,6 +122,8 @@ while [[ $# -gt 0 ]]; do
96
122
  --if-rev) [ $# -ge 2 ] || error_exit "--if-rev requires a value"; IF_REV="$2"; shift 2 ;;
97
123
  --limit) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
98
124
  --after) [ $# -ge 2 ] || error_exit "--after requires a value"; AFTER="$2"; shift 2 ;;
125
+ --upload) [ $# -ge 2 ] || error_exit "--upload requires a file path"; OP="upload"; UPLOAD="$2"; shift 2 ;;
126
+ --name) [ $# -ge 2 ] || error_exit "--name requires a value"; NAME="$2"; shift 2 ;;
99
127
  --request-access) OP="request-access"; shift ;;
100
128
  --collaborators) OP="collaborators"; shift ;;
101
129
  --owner) OP="owner"; shift ;;
@@ -109,13 +137,21 @@ while [[ $# -gt 0 ]]; do
109
137
  done
110
138
 
111
139
  [ -n "$APP" ] || error_exit "--app is required"
112
- [ -n "$OP" ] || error_exit "one of --list / --get / --set / --update / --add / --delete / --request-access / --collaborators / --owner / --add-collaborator is required"
140
+ [ -n "$OP" ] || error_exit "one of --list / --get / --set / --update / --add / --delete / --upload / --request-access / --collaborators / --owner / --add-collaborator is required"
113
141
  [[ "$APP" =~ ^[a-z0-9]{10}$ ]] || error_exit "--app must be a 10-character app id"
114
142
  case "$COLL" in .|..) error_exit "collection cannot be . or .." ;; esac
115
143
  case "$DOC" in .|..) error_exit "doc id cannot be . or .." ;; esac
116
144
  BASE="/apps/${APP}/data/$(uri "$COLL")"
117
145
 
118
146
  case "$OP" in
147
+ upload)
148
+ FILE_PATH="$(read_data_file "$UPLOAD" path)" || exit 1
149
+ [ -n "$NAME" ] || NAME="$(basename "$FILE_PATH")"
150
+ RESPONSE=$(CREWLY_API_UPLOAD_FILE="$FILE_PATH" CREWLY_API_UPLOAD_TYPE="$(content_type_for "$FILE_PATH")" CREWLY_API_UPLOAD_NAME="$(uri "$NAME")" call POST "/apps/${APP}/files") || { printf '%s\n' "$RESPONSE"; exit 1; }
151
+ printf '%s' "$RESPONSE" | jq -e '.data.url | type == "string" and length > 0' >/dev/null 2>&1 || error_exit "the upload answered without a url; the file may not be stored"
152
+ printf '%s' "$RESPONSE" | jq -c '{success: true, fileId: .data.fileId, name: .data.name, size: .data.size, contentType: .data.contentType, url: .data.url}'
153
+ exit 0
154
+ ;;
119
155
  request-access)
120
156
  case "$SCOPE" in ""|team|agent) ;; *) error_exit "--scope is team or agent" ;; esac
121
157
  BODY=$(jq -cn --arg s "$SCOPE" --arg r "$REASON" '{} + (if $s != "" then {scope: $s} else {} end) + (if $r != "" then {reason: $r} else {} end)')
@@ -51,7 +51,20 @@ class H(BaseHTTPRequestHandler):
51
51
  def _body(self, m):
52
52
  n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
53
53
  self._reply(m, json.loads(body or '{}'))
54
- def do_POST(self): self._body('POST')
54
+ def do_POST(self):
55
+ if self.path.endswith('/files'):
56
+ n = int(self.headers.get('content-length', '0')); raw = self.rfile.read(n)
57
+ name = self.headers.get('x-file-name', '')
58
+ open(LOG, 'w').write(json.dumps({'method': 'POST', 'path': self.path, 'ct': self.headers.get('content-type'), 'name': name, 'len': n, 'hex': raw.hex(), 'session': self.headers.get('x-agent-session')}))
59
+ def send(code, obj):
60
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
61
+ self.wfile.write(json.dumps(obj).encode())
62
+ if name == 'nourl.png':
63
+ return send(201, {'success': True, 'data': {'fileId': 'f1'}})
64
+ if name == 'refuse.png':
65
+ return send(403, {'success': False, 'error': 'not_your_app', 'message': 'This app is not yours.'})
66
+ return send(201, {'success': True, 'data': {'fileId': 'f1', 'name': name, 'size': n, 'contentType': self.headers.get('content-type'), 'url': 'https://apps.crewlyai.com/_f/tok/f1'}})
67
+ self._body('POST')
55
68
  def do_PUT(self): self._body('PUT')
56
69
  def do_PATCH(self): self._body('PATCH')
57
70
  def log_message(self, *a, **k): pass
@@ -149,5 +162,38 @@ check "set-owner needs a value" "$(printf '%s' "$OUT" | grep -c 'requires an own
149
162
  run --app $A --set items milk --data '{"done":true}' >/dev/null
150
163
  check "--set is still a data write" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "PUT /api/apps/$A/data/items/milk"
151
164
 
165
+ # --upload: raw bytes, content type from the extension, name URI-encoded; same project-dir guard as --data-file.
166
+ printf '\x89PNG\r\n\x1a\n\x00\x01' > "$WORK/proj/pic one.png"
167
+ OUT=$(run --app $A --upload "$WORK/proj/pic one.png")
168
+ check "upload: output has the url" "$(printf '%s' "$OUT" | jq -c '{success, fileId, name, size, contentType, url}')" '{"success":true,"fileId":"f1","name":"pic%20one.png","size":10,"contentType":"image/png","url":"https://apps.crewlyai.com/_f/tok/f1"}'
169
+ check "upload: request" "$(jq -c '{method, path, ct, name, len, hex, session}' "$STUB_LOG")" "{\"method\":\"POST\",\"path\":\"/api/apps/$A/files\",\"ct\":\"image/png\",\"name\":\"pic%20one.png\",\"len\":10,\"hex\":\"89504e470d0a1a0a0001\",\"session\":\"dev-ella\"}"
170
+ OUT=$(run --app $A --upload "$WORK/proj/pic one.png" --name "shot 1.png")
171
+ check "upload: --name overrides" "$(jq -r .name "$STUB_LOG")" "shot%201.png"
172
+ printf '{"a":1}' > "$WORK/proj/data.json"
173
+ run --app $A --upload "$WORK/proj/data.json" >/dev/null
174
+ check "upload: json is sent as octet-stream" "$(jq -r .ct "$STUB_LOG")" "application/octet-stream"
175
+ # A normal call after an upload is JSON again (the upload env does not leak).
176
+ run --app $A --add items --data '{"n":1}' >/dev/null
177
+ check "upload does not leak into the next call" "$(jq -c .body "$STUB_LOG")" '{"data":{"n":1}}'
178
+ printf 'x' > "$WORK/outside.png"
179
+ OUT=$(run_err --app $A --upload "$WORK/outside.png" || true)
180
+ check "upload: outside the project refused" "$(printf '%s' "$OUT" | grep -c 'outside your project directory')" "1"
181
+ ln -s "$WORK/proj/pic one.png" "$WORK/proj/link.png"
182
+ OUT=$(run_err --app $A --upload "$WORK/proj/link.png" || true)
183
+ check "upload: symlink refused" "$(printf '%s' "$OUT" | grep -c 'symbolic link')" "1"
184
+ OUT=$(run_err --app $A --upload "$WORK/proj/nope.png" || true)
185
+ check "upload: missing file" "$(printf '%s' "$OUT" | grep -c 'file not found')" "1"
186
+ : > "$WORK/proj/empty.png"
187
+ OUT=$(run_err --app $A --upload "$WORK/proj/empty.png" || true)
188
+ check "upload: empty file refused" "$(printf '%s' "$OUT" | grep -c 'is empty')" "1"
189
+ printf '\x89PNG' > "$WORK/proj/refuse.png"
190
+ OUT=$(run --app $A --upload "$WORK/proj/refuse.png"; true)
191
+ check "upload: Cloud refusal passed through, not a fake success" "$(printf '%s' "$OUT" | jq -c '{success, status, reason}')" '{"success":false,"status":403,"reason":"not_your_app"}'
192
+ printf '\x89PNG' > "$WORK/proj/nourl.png"
193
+ OUT=$(run_err --app $A --upload "$WORK/proj/nourl.png" || true)
194
+ check "upload: 2xx without a url fails, not success with nulls" "$(printf '%s' "$OUT" | grep -c 'without a url')" "1"
195
+ OUT=$(run_err --app $A --upload || true)
196
+ check "upload needs a path" "$(printf '%s' "$OUT" | grep -c 'requires a file path')" "1"
197
+
152
198
  echo "app-data: $PASS passed, $FAIL failed"
153
199
  [ "$FAIL" -eq 0 ]
@@ -88,7 +88,7 @@ Output:
88
88
  `--list`) — never publish a second app with the same name; the owner then
89
89
  sees two. Limits:
90
90
  300 files, 5 MB per file, 25 MB per version. The last 10 versions are kept.
91
- - Read and write the app's data from your side with `app-data`.
91
+ - Read and write the app's data from your side with `app-data`. To put an image into the app, `app-data --app <id> --upload <file>` returns a url you can store in a document.
92
92
 
93
93
  ## Public apps (only the owner can make one)
94
94
 
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: Slack File
3
+ description: Get a Slack file by link or id and save it locally — also a file posted in a channel your own bot is not in, or by an agent on another machine. Prints the saved path and, for text files, the first lines.
4
+ version: 1.0.0
5
+ category: communication
6
+ skillType: claude-skill
7
+ triggers:
8
+ - can't open the slack file
9
+ - read this slack attachment
10
+ - download the file from slack
11
+ - 打不开 slack 文件
12
+ - 读一下这个附件
13
+ tags:
14
+ - slack
15
+ - file
16
+ - attachment
17
+ execution:
18
+ type: script
19
+ script:
20
+ file: execute.sh
21
+ interpreter: bash
22
+ timeoutMs: 120000
23
+ ---
24
+
25
+ # Slack File
26
+
27
+ When you see a Slack file you cannot open — a link a colleague pasted, a
28
+ file posted in another channel, `[Slack: ... could not be read]` — fetch it
29
+ yourself. **Never ask a colleague to paste the text.**
30
+
31
+ ```bash
32
+ bash execute.sh get https://acme.slack.com/files/U0ELLA1234/F0ABC12345/longform-en.md
33
+ bash execute.sh get F0ABC12345
34
+ bash execute.sh get https://acme.slack.com/archives/C0MKTG1234/p1696771234567890 # a message with a file
35
+ bash execute.sh get F0ABC12345 --out /path/to/project/longform-en.md
36
+ ```
37
+
38
+ It tries this machine's Slack bots first (yours, the uploader's, the
39
+ channel's agents, the workspace bot), then Crewly Cloud, which holds every
40
+ bot of your account on every machine.
41
+
42
+ Output: `{ "path", "name", "mimetype", "size", "via", "preview"? }`.
43
+ `path` is where the file is saved (`~/.crewly/tmp/slack-files/` unless
44
+ `--out` is given); `preview` holds the first lines of a text file. Read
45
+ the full file at `path`.
46
+
47
+ | Error `code` | Meaning / what to do |
48
+ |---|---|
49
+ | `not_visible` | No bot of your account is in the file's channel. Ask the owner to invite you (or the Crewly app) there. |
50
+ | `foreign_workspace` | The link is from a Slack workspace your account has not connected. |
51
+ | `file_deleted` | The file was deleted. |
52
+ | `too_large` | Over 25 MB. |
53
+ | `no_file_in_message` | The message link has no file attached. |
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # slack-file — get a Slack file by link or id (also one your own bot cannot see).
3
+ #
4
+ # Backed by POST /api/slack/files/fetch: this machine's bot tokens first, then
5
+ # Crewly Cloud (every bot of the account).
6
+ #
7
+ # Usage: execute.sh get <link|file id> [--out <path>]
8
+ set -euo pipefail
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ # shellcheck source=/dev/null
11
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
12
+
13
+ usage() {
14
+ cat <<'USAGE'
15
+ slack-file — get a Slack file by link or id
16
+
17
+ get <link|id> File link (https://<ws>.slack.com/files/...), file id (F...),
18
+ download URL, or a message link carrying a file (required)
19
+ --out <path> Also copy the file to this path
20
+ USAGE
21
+ }
22
+
23
+ CMD=""; REF=""; OUT=""
24
+ while [ $# -gt 0 ]; do
25
+ case "$1" in
26
+ get) CMD="get"; REF="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
27
+ --out|-o) OUT="${2:-}"; shift 2 ;;
28
+ -h|--help) usage; exit 0 ;;
29
+ *) if [ -z "$REF" ] && [ "$CMD" = "get" ]; then REF="$1"; shift; else echo "{\"error\":\"Unknown argument: $1\"}" >&2; exit 1; fi ;;
30
+ esac
31
+ done
32
+
33
+ if [ "$CMD" != "get" ] || [ -z "$REF" ]; then
34
+ echo '{"error":"Usage: execute.sh get <link|file id> [--out <path>]"}' >&2
35
+ usage >&2
36
+ exit 1
37
+ fi
38
+
39
+ # Slack mrkdwn links arrive as <url|label>; the backend unwraps them too.
40
+ BODY=$(jq -cn --arg fileRef "$REF" '{fileRef: $fileRef}')
41
+
42
+ # Test hook: print the request instead of calling the backend.
43
+ if [ "${CREWLY_SKILL_DRY_RUN:-}" = "1" ]; then
44
+ echo "POST /slack/files/fetch $BODY"
45
+ exit 0
46
+ fi
47
+
48
+ if ! RESPONSE=$(api_call POST "/slack/files/fetch" "$BODY" 2>&1); then
49
+ echo "$RESPONSE" >&2
50
+ exit 1
51
+ fi
52
+
53
+ FILE_PATH=$(printf '%s' "$RESPONSE" | jq -r '.data.path // empty')
54
+ if [ -z "$FILE_PATH" ]; then
55
+ echo "$RESPONSE" >&2
56
+ exit 1
57
+ fi
58
+
59
+ if [ -n "$OUT" ]; then
60
+ mkdir -p "$(dirname "$OUT")"
61
+ cp "$FILE_PATH" "$OUT"
62
+ FILE_PATH="$OUT"
63
+ fi
64
+
65
+ printf '%s' "$RESPONSE" | jq --arg path "$FILE_PATH" '{
66
+ path: $path,
67
+ name: .data.file.name,
68
+ mimetype: .data.file.mimetype,
69
+ size: .data.file.size,
70
+ via: .data.file.via
71
+ } + (if .data.preview then {preview: .data.preview} else {} end)
72
+ + (if (.data.file.otherFileIds // []) | length > 0 then {otherFileIds: .data.file.otherFileIds} else {} end)'
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ # Tests for slack-file: argument parsing, request body, --out copy.
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PASS=0; FAIL=0
6
+ assert_eq() { if [ "$2" = "$3" ]; then echo "PASS: $1"; PASS=$((PASS+1)); else echo "FAIL: $1 — expected '$2', got '$3'"; FAIL=$((FAIL+1)); fi; }
7
+ assert_contains() { if [[ "$3" == *"$2"* ]]; then echo "PASS: $1"; PASS=$((PASS+1)); else echo "FAIL: $1 — expected '$2' in: $3"; FAIL=$((FAIL+1)); fi; }
8
+
9
+ LINK='https://acme.slack.com/files/U0ELLA1234/F0ABC12345/longform-en.md'
10
+ assert_eq "get <link> → POST with fileRef" "POST /slack/files/fetch {\"fileRef\":\"$LINK\"}" "$(CREWLY_SKILL_DRY_RUN=1 bash "$SCRIPT_DIR/execute.sh" get "$LINK")"
11
+ assert_eq "get <id>" 'POST /slack/files/fetch {"fileRef":"F0ABC12345"}' "$(CREWLY_SKILL_DRY_RUN=1 bash "$SCRIPT_DIR/execute.sh" get F0ABC12345)"
12
+ assert_eq "quotes in the ref stay JSON-safe" 'POST /slack/files/fetch {"fileRef":"F0\"x"}' "$(CREWLY_SKILL_DRY_RUN=1 bash "$SCRIPT_DIR/execute.sh" get 'F0"x')"
13
+ assert_contains "missing ref → usage error" "Usage" "$(bash "$SCRIPT_DIR/execute.sh" get 2>&1 || true)"
14
+ assert_contains "--help prints usage" "slack-file" "$(bash "$SCRIPT_DIR/execute.sh" --help)"
15
+
16
+ # --out copies the saved file and reports the new path (api_call stubbed).
17
+ TMP="$(mktemp -d)"
18
+ trap 'rm -rf "$TMP"' EXIT
19
+ printf 'hello\n' > "$TMP/saved.md"
20
+ STUB="$TMP/stub-lib.sh"
21
+ cat > "$STUB" <<STUB_EOF
22
+ api_call() { printf '%s' '{"success":true,"data":{"path":"$TMP/saved.md","file":{"name":"longform-en.md","mimetype":"text/markdown","size":6,"via":"cloud:agent:team-ella"},"preview":"hello"}}'; }
23
+ STUB_EOF
24
+ mkdir -p "$TMP/skills/agent/core/slack-file" "$TMP/skills/agent/_common"
25
+ cp "$SCRIPT_DIR/execute.sh" "$TMP/skills/agent/core/slack-file/execute.sh"
26
+ cp "$STUB" "$TMP/skills/agent/_common/lib.sh"
27
+ OUT_JSON="$(bash "$TMP/skills/agent/core/slack-file/execute.sh" get "$LINK" --out "$TMP/out/longform-en.md")"
28
+ assert_eq "--out path reported" "$TMP/out/longform-en.md" "$(printf '%s' "$OUT_JSON" | jq -r .path)"
29
+ assert_eq "--out file copied" "hello" "$(cat "$TMP/out/longform-en.md")"
30
+ assert_eq "preview passed through" "hello" "$(printf '%s' "$OUT_JSON" | jq -r .preview)"
31
+ assert_eq "via passed through" "cloud:agent:team-ella" "$(printf '%s' "$OUT_JSON" | jq -r .via)"
32
+
33
+ echo "=== Results: $PASS passed, $FAIL failed ==="
34
+ [ "$FAIL" -eq 0 ]
@@ -4114,6 +4114,35 @@ export declare const SLACK_RECONNECT_CONSTANTS: {
4114
4114
  /** Number of consecutive ping failures before forcing a reconnect */
4115
4115
  readonly PING_FAILURES_BEFORE_RECONNECT: 2;
4116
4116
  };
4117
+ /**
4118
+ * Fetching a Slack file by link or id (`slack-file get`, and files linked
4119
+ * in an inbound message). Local bot tokens first, then Crewly Cloud, which
4120
+ * holds every bot of the account (`POST /api/cloud/slack/files/fetch`).
4121
+ */
4122
+ export declare const SLACK_FILE_FETCH_CONSTANTS: {
4123
+ /** Largest file fetched (bytes) — same cap as Cloud */
4124
+ readonly MAX_BYTES: number;
4125
+ /** Cloud endpoint, appended to the cloud URL */
4126
+ readonly CLOUD_FETCH_PATH: "/api/cloud/slack/files/fetch";
4127
+ /** Header carrying the file metadata on Cloud's raw answer (base64url JSON) */
4128
+ readonly CLOUD_META_HEADER: "x-crewly-slack-file";
4129
+ /** Local bot tokens tried at most */
4130
+ readonly MAX_LOCAL_CANDIDATES: 12;
4131
+ /** Slack API / Cloud call timeout (ms) */
4132
+ readonly API_TIMEOUT_MS: 15000;
4133
+ /** Download timeout (ms) */
4134
+ readonly DOWNLOAD_TIMEOUT_MS: 60000;
4135
+ /** Redirect hops followed on a download */
4136
+ readonly MAX_REDIRECTS: 5;
4137
+ /** Linked files auto-fetched per inbound message */
4138
+ readonly MAX_LINKED_PER_MESSAGE: 3;
4139
+ /** Linked files larger than this are not auto-fetched (the agent can still run `slack-file get`) */
4140
+ readonly MAX_LINKED_BYTES: number;
4141
+ /** Lines of a text file shown inline by the skill */
4142
+ readonly PREVIEW_LINES: 20;
4143
+ /** Characters of a text file shown inline by the skill */
4144
+ readonly PREVIEW_CHARS: 2000;
4145
+ };
4117
4146
  export declare const SLACK_FILE_DOWNLOAD_CONSTANTS: {
4118
4147
  /** Temp directory for downloaded files (relative to ~/.crewly/) */
4119
4148
  readonly TEMP_DIR: "tmp/slack-files";
@@ -4656,6 +4685,9 @@ export declare const CREWLY_APPS_CONSTANTS: {
4656
4685
  readonly PUBLISH_CLOSE_ABOVE_BYTES: number;
4657
4686
  /** The publish route, for the dedicated body parser registered before the global one */
4658
4687
  readonly PUBLISH_ROUTE: "/api/apps/publish";
4688
+ /** A file upload to an app (POST /api/apps/:appId/files): body cap here; Cloud applies its own, smaller one and its answer is passed through */
4689
+ readonly UPLOAD_BODY_LIMIT: "10mb";
4690
+ readonly UPLOAD_BODY_MAX_BYTES: number;
4659
4691
  /** Cloud request timeout (publish uploads can be large) */
4660
4692
  readonly REQUEST_TIMEOUT_MS: 120000;
4661
4693
  /** Default entry file of a bundle */
@@ -7225,6 +7257,33 @@ export declare const REMOTE_MCP_CONSTANTS: {
7225
7257
  readonly MAX_TOOL_NAMES: 50;
7226
7258
  /** Catalog presets the add form offers. */
7227
7259
  readonly PROVIDERS: readonly string[];
7260
+ /**
7261
+ * OAuth state (clients, tokens, pending sign-ins) under CREWLY_HOME:
7262
+ * AES-256-GCM with the install's master key, mode 0600.
7263
+ */
7264
+ readonly OAUTH_STORE_FILE: "remote-mcp-oauth.enc";
7265
+ /** Master key the OAuth store is encrypted with (under CREWLY_HOME). */
7266
+ readonly OAUTH_MASTER_KEY_FILE: "credentials/master.key";
7267
+ /** Cloud broker that hosts the public redirect URI (crewly-services auth). */
7268
+ readonly OAUTH_CLOUD_PATH: "/api/cloud/mcp-oauth";
7269
+ /** Client name sent in dynamic client registration. */
7270
+ readonly OAUTH_CLIENT_NAME: "Crewly";
7271
+ /** Refresh an access token this long before it expires (Zoho's last ~1 h). */
7272
+ readonly OAUTH_REFRESH_MARGIN_MS: number;
7273
+ /** Assumed lifetime when the token endpoint omits expires_in. */
7274
+ readonly OAUTH_DEFAULT_TOKEN_TTL_MS: number;
7275
+ /** Discovery / registration / token request timeout. */
7276
+ readonly OAUTH_REQUEST_TIMEOUT_MS: 15000;
7277
+ /** Poll the Cloud broker this often while the link is untapped. */
7278
+ readonly OAUTH_POLL_SLOW_MS: 20000;
7279
+ /** ...and this often once the owner has opened it. */
7280
+ readonly OAUTH_POLL_FAST_MS: 2000;
7281
+ /** Post the "sign in" card for a server at most this often while its link is unused. */
7282
+ readonly OAUTH_CARD_THROTTLE_MS: number;
7283
+ /** Agent → backend proxy path suffix: `/api/connectors/remote-mcp/<id>/mcp`. */
7284
+ readonly PROXY_SUFFIX: "/mcp";
7285
+ /** JSON-RPC error code the proxy answers with when the owner must sign in. */
7286
+ readonly PROXY_NEEDS_AUTH_RPC_CODE: -32001;
7228
7287
  };
7229
7288
  /**
7230
7289
  * Reconciler wake pacing (CREW-304). On 2026-10-06 the reconciler traced