crewly 1.20.251 → 1.20.253

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 (94) hide show
  1. package/config/skills/agent/core/app-comments/SKILL.md +25 -1
  2. package/config/skills/agent/core/app-comments/execute.sh +12 -3
  3. package/config/skills/agent/core/app-comments/execute.test.sh +17 -0
  4. package/config/skills/agent/core/app-data/SKILL.md +65 -3
  5. package/config/skills/agent/core/app-data/execute.sh +51 -3
  6. package/config/skills/agent/core/app-data/execute.test.sh +42 -0
  7. package/config/skills/agent/core/publish-app/SKILL.md +5 -1
  8. package/config/skills/agent/core/publish-app/execute.sh +26 -3
  9. package/config/skills/agent/core/publish-app/execute.test.sh +18 -0
  10. package/dist/backend/backend/src/constants.d.ts +36 -0
  11. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  12. package/dist/backend/backend/src/constants.js +44 -0
  13. package/dist/backend/backend/src/constants.js.map +1 -1
  14. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +24 -0
  15. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  16. package/dist/backend/backend/src/controllers/apps/apps.controller.js +47 -1
  17. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  18. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +8 -0
  19. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  20. package/dist/backend/backend/src/controllers/apps/apps.routes.js +31 -2
  21. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  22. package/dist/backend/backend/src/index.d.ts.map +1 -1
  23. package/dist/backend/backend/src/index.js +9 -0
  24. package/dist/backend/backend/src/index.js.map +1 -1
  25. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  26. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  27. package/dist/backend/backend/src/services/apps/app-collaborators.service.d.ts +103 -0
  28. package/dist/backend/backend/src/services/apps/app-collaborators.service.d.ts.map +1 -0
  29. package/dist/backend/backend/src/services/apps/app-collaborators.service.js +161 -0
  30. package/dist/backend/backend/src/services/apps/app-collaborators.service.js.map +1 -0
  31. package/dist/backend/backend/src/services/apps/app-comment-audio.service.d.ts +164 -0
  32. package/dist/backend/backend/src/services/apps/app-comment-audio.service.d.ts.map +1 -0
  33. package/dist/backend/backend/src/services/apps/app-comment-audio.service.js +217 -0
  34. package/dist/backend/backend/src/services/apps/app-comment-audio.service.js.map +1 -0
  35. package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts +156 -0
  36. package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts.map +1 -0
  37. package/dist/backend/backend/src/services/apps/app-comment-room.service.js +344 -0
  38. package/dist/backend/backend/src/services/apps/app-comment-room.service.js.map +1 -0
  39. package/dist/backend/backend/src/services/apps/app-comments-slack.service.d.ts +44 -2
  40. package/dist/backend/backend/src/services/apps/app-comments-slack.service.d.ts.map +1 -1
  41. package/dist/backend/backend/src/services/apps/app-comments-slack.service.js +90 -10
  42. package/dist/backend/backend/src/services/apps/app-comments-slack.service.js.map +1 -1
  43. package/dist/backend/backend/src/services/apps/app-roster.service.d.ts +37 -1
  44. package/dist/backend/backend/src/services/apps/app-roster.service.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/apps/app-roster.service.js +62 -7
  46. package/dist/backend/backend/src/services/apps/app-roster.service.js.map +1 -1
  47. package/dist/backend/backend/src/services/apps/app-wake-message.d.ts +25 -1
  48. package/dist/backend/backend/src/services/apps/app-wake-message.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/apps/app-wake-message.js +25 -13
  50. package/dist/backend/backend/src/services/apps/app-wake-message.js.map +1 -1
  51. package/dist/backend/backend/src/services/apps/app-wake.service.d.ts +34 -0
  52. package/dist/backend/backend/src/services/apps/app-wake.service.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/apps/app-wake.service.js +97 -17
  54. package/dist/backend/backend/src/services/apps/app-wake.service.js.map +1 -1
  55. package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts +25 -0
  56. package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/apps/apps-cloud.client.js +41 -4
  58. package/dist/backend/backend/src/services/apps/apps-cloud.client.js.map +1 -1
  59. package/dist/backend/backend/src/services/apps/apps.service.d.ts +115 -2
  60. package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/apps/apps.service.js +181 -9
  62. package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
  63. package/dist/backend/backend/src/services/apps/apps.wiring.d.ts +70 -1
  64. package/dist/backend/backend/src/services/apps/apps.wiring.d.ts.map +1 -1
  65. package/dist/backend/backend/src/services/apps/apps.wiring.js +274 -5
  66. package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
  67. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +1 -1
  68. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  69. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +3 -0
  70. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  71. package/dist/backend/backend/src/services/decisions/decision.service.d.ts +2 -0
  72. package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  73. package/dist/backend/backend/src/services/decisions/decision.service.js +1 -0
  74. package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
  75. package/dist/backend/backend/src/types/decision.types.d.ts +27 -2
  76. package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -1
  77. package/dist/backend/build-info.json +2 -2
  78. package/dist/cli/backend/src/constants.d.ts +36 -0
  79. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  80. package/dist/cli/backend/src/constants.js +44 -0
  81. package/dist/cli/backend/src/constants.js.map +1 -1
  82. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  83. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  84. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +1 -1
  85. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  86. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +3 -0
  87. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  88. package/dist/cli/backend/src/services/decisions/decision.service.d.ts +2 -0
  89. package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  90. package/dist/cli/backend/src/services/decisions/decision.service.js +1 -0
  91. package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
  92. package/dist/cli/backend/src/types/decision.types.d.ts +27 -2
  93. package/dist/cli/backend/src/types/decision.types.d.ts.map +1 -1
  94. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: App Comments
3
3
  description: The owner's comments on a Crewly App — each one points at an element they tapped in the app (data-crewly-id, CSS selector, text, outerHTML, position). List them, reply in the thread, resolve after you addressed one, reopen. Use it when an [APP CHANGES] message says the owner commented on your app or @mentioned you in a comment.
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  category: productivity
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -52,6 +52,20 @@ bash execute.sh --app 28au74d9cj --resolve Xk3_9aQ --text "Done in version 5: th
52
52
  bash execute.sh --app 28au74d9cj --reopen Xk3_9aQ
53
53
  ```
54
54
 
55
+ **Voice comments.** The owner can record a comment instead of typing it (a mic
56
+ button in the app's comment box). Crewly downloads the recording when it
57
+ delivers the comment, and the `[APP CHANGES]` message names the file:
58
+ `Voice comment (0:42): /…/.crewly/tmp/app-comment-audio/<app>-<id>.webm — transcribe it with the transcribe-audio skill before acting`.
59
+ Transcribe it first (`{"audioFile": "<path>"}`), then act on what the owner said.
60
+ If it was not downloaded yet, or you need it again, fetch it:
61
+
62
+ ```bash
63
+ bash execute.sh --app 28au74d9cj --audio Xk3_9aQ # → {"recordings":[{"path":"…","durationMs":42000,…}]}
64
+ ```
65
+
66
+ In `--list` / `--get`, a comment or reply with a recording shows `"voice":[{"seconds":42}]`
67
+ (its text may be empty). Reply in text as usual.
68
+
55
69
  Comments are mirrored to Slack (the owner's DM with you, else your team channel), one thread per comment; your `--reply` / `--resolve` show up in that thread, and the owner's replies there come back to you as comment replies. Nothing to do for that.
56
70
 
57
71
  Output (`--list`):
@@ -64,6 +78,16 @@ Output (`--list`):
64
78
  "comment":"Make this green","at":"…","replies":[],"resolvedBy":null}]}
65
79
  ```
66
80
 
81
+ ## Apps owned by a team or a channel
82
+
83
+ If the app's owner is a team or a Crewly channel (`app-data --owner`), each
84
+ comment arrives as a thread in that room (and its Slack channel), not as an
85
+ `[APP CHANGES]` message: "💬 Comment on Daily brief (#3) — Heading “Today”: …".
86
+ The room's rules decide who answers. Reply **in that thread** (as for any room
87
+ message): your reply is added to the comment in the app, so do not also run
88
+ `--reply`. Use `--resolve <id>` when it is done; that shows in the thread too.
89
+ The owner's replies in the thread (in Slack or in the app) reach the room once.
90
+
67
91
  ## Finding the element
68
92
 
69
93
  `on` is what the app's page reported for the tapped element:
@@ -21,6 +21,7 @@ Usage:
21
21
  bash execute.sh --app <appId> --reply <commentId> --text "Made it green in version 5."
22
22
  bash execute.sh --app <appId> --resolve <commentId> [--text "what you changed"]
23
23
  bash execute.sh --app <appId> --reopen <commentId>
24
+ bash execute.sh --app <appId> --audio <commentId> # download its voice recordings; prints local paths
24
25
 
25
26
  Options:
26
27
  --app App id (from publish-app)
@@ -44,10 +45,13 @@ call() {
44
45
 
45
46
  # A thread, compact: who wrote what, and the full anchor (the element in the app).
46
47
  # `to` lists the agents the owner @mentioned (only when there are any).
48
+ # `voice` lists voice recordings (fetch them with --audio <id>, then transcribe).
47
49
  THREAD='({id, number, status, version, on: .anchor, comment: .body, at: .createdAt}
48
50
  + (if ((.mentions // []) | length) > 0 then {to: [.mentions[].name]} else {} end)
51
+ + (if ((.attachments // []) | length) > 0 then {voice: [.attachments[] | select(.kind == "audio") | {seconds: ((.durationMs // 0) / 1000 | floor)}]} else {} end)
49
52
  + {replies: [.replies[]? | ({from: (if .author.kind == "owner" then "owner" else (.author.name // "agent") end), text: .body, at: .createdAt}
50
- + (if ((.mentions // []) | length) > 0 then {to: [.mentions[].name]} else {} end))],
53
+ + (if ((.mentions // []) | length) > 0 then {to: [.mentions[].name]} else {} end)
54
+ + (if ((.attachments // []) | length) > 0 then {voice: [.attachments[] | select(.kind == "audio") | {seconds: ((.durationMs // 0) / 1000 | floor)}]} else {} end))],
51
55
  resolvedBy: (if .resolvedBy then (if .resolvedBy.kind == "owner" then "owner" else .resolvedBy.name end) else null end)})'
52
56
 
53
57
  APP=""; OP=""; ID=""; STATUS=""; TEXT=""; HAS_TEXT=0
@@ -55,7 +59,7 @@ while [[ $# -gt 0 ]]; do
55
59
  case "$1" in
56
60
  --app) [ $# -ge 2 ] || error_exit "--app requires a value"; APP="$2"; shift 2 ;;
57
61
  --list) OP="list"; shift ;;
58
- --get|--reply|--resolve|--reopen)
62
+ --get|--reply|--resolve|--reopen|--audio)
59
63
  [ $# -ge 2 ] || error_exit "$1 requires a comment id"
60
64
  OP="${1#--}"; ID="$2"; shift 2 ;;
61
65
  --status) [ $# -ge 2 ] || error_exit "--status requires a value"; STATUS="$2"; shift 2 ;;
@@ -66,7 +70,7 @@ while [[ $# -gt 0 ]]; do
66
70
  done
67
71
 
68
72
  [ -n "$APP" ] || error_exit "--app is required"
69
- [ -n "$OP" ] || error_exit "one of --list / --get / --reply / --resolve / --reopen is required"
73
+ [ -n "$OP" ] || error_exit "one of --list / --get / --reply / --resolve / --reopen / --audio is required"
70
74
  [[ "$APP" =~ ^[a-z0-9]{10}$ ]] || error_exit "--app must be a 10-character app id"
71
75
  if [ -n "$ID" ]; then
72
76
  [[ "$ID" =~ ^[A-Za-z0-9_-]{1,32}$ ]] || error_exit "comment id must be the id from --list"
@@ -96,6 +100,11 @@ case "$OP" in
96
100
  RESPONSE=$(reply) || { printf '%s\n' "$RESPONSE"; exit 1; }
97
101
  printf '%s' "$RESPONSE" | jq -c '{success: true, id: .data.id, number: .data.number, replies: (.data.replies | length), status: .data.status}'
98
102
  ;;
103
+ audio)
104
+ RESPONSE=$(call POST "${BASE}/${ID}/audio" '{}') || { printf '%s\n' "$RESPONSE"; exit 1; }
105
+ printf '%s' "$RESPONSE" | jq -c '{success: true, recordings: .data.recordings,
106
+ next: (if ((.data.recordings // []) | length) == 0 then "This thread has no voice recordings." else "Transcribe each path with the transcribe-audio skill ({\"audioFile\": \"<path>\"}) before acting." end)}'
107
+ ;;
99
108
  resolve|reopen)
100
109
  if [ "$HAS_TEXT" = 1 ]; then
101
110
  R=$(reply) || { printf '%s\n' "$R"; exit 1; }
@@ -38,6 +38,14 @@ class H(BaseHTTPRequestHandler):
38
38
  t['replies'] = [{'id': 'r2', 'body': '@Nova too', 'author': {'kind': 'owner', 'name': 'Owner'}, 'createdAt': 't3',
39
39
  'mentions': [{'session': 'crewly-ops-nova-99887766', 'name': 'Nova', 'instanceId': 'i2'}]}]
40
40
  return send(200, {'success': True, 'data': t})
41
+ if method == 'GET' and self.path.endswith('/comments/v1'):
42
+ t = dict(THREAD); t['id'] = 'v1'; t['body'] = ''
43
+ t['attachments'] = [{'kind': 'audio', 'blobId': 'abcdefgh12345678', 'mime': 'audio/webm', 'durationMs': 42500, 'size': 9000}]
44
+ return send(200, {'success': True, 'data': t})
45
+ if method == 'POST' and self.path.endswith('/comments/v1/audio'):
46
+ return send(200, {'success': True, 'data': {'recordings': [{'blobId': 'abcdefgh12345678', 'durationMs': 42500, 'mime': 'audio/webm', 'path': '/h/.crewly/tmp/app-comment-audio/x.webm'}]}})
47
+ if method == 'POST' and self.path.endswith('/comments/c1/audio'):
48
+ return send(200, {'success': True, 'data': {'recordings': []}})
41
49
  t = dict(THREAD)
42
50
  if self.path.endswith('/resolve'):
43
51
  t['status'] = 'resolved'; t['resolvedBy'] = {'kind': 'agent', 'name': 'Ella'}
@@ -86,6 +94,15 @@ check "resolve: output" "$OUT" '{"success":true,"id":"c1","number":3,"status":"r
86
94
  run --app $A --reopen c1 >/dev/null
87
95
  check "reopen" "$(reqs)" "{\"method\":\"POST\",\"path\":\"/api/apps/$A/comments/c1/reopen\",\"body\":null}"
88
96
 
97
+ OUT=$(run --app $A --get v1)
98
+ check "get: voice recordings listed" "$(printf '%s' "$OUT" | jq -c '{comment: .comment.comment, voice: .comment.voice}')" '{"comment":"","voice":[{"seconds":42}]}'
99
+
100
+ OUT=$(run --app $A --audio v1)
101
+ check "audio: request" "$(reqs)" "{\"method\":\"POST\",\"path\":\"/api/apps/$A/comments/v1/audio\",\"body\":{}}"
102
+ check "audio: paths + next step" "$(printf '%s' "$OUT" | jq -c '{success, path: .recordings[0].path, next}')" '{"success":true,"path":"/h/.crewly/tmp/app-comment-audio/x.webm","next":"Transcribe each path with the transcribe-audio skill ({\"audioFile\": \"<path>\"}) before acting."}'
103
+ OUT=$(run --app $A --audio c1)
104
+ check "audio: none" "$(printf '%s' "$OUT" | jq -r .next)" "This thread has no voice recordings."
105
+
89
106
  OUT=$(run --app $A --get missing; true)
90
107
  check "missing → not_found" "$(printf '%s' "$OUT" | jq -c '{success, status, reason}')" '{"success":false,"status":404,"reason":"not_found"}'
91
108
 
@@ -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.2.0
4
+ version: 1.4.0
5
5
  category: productivity
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -44,6 +44,11 @@ bash execute.sh --app 28au74d9cj --set meta settings --data '{"title":"Weekly sh
44
44
  bash execute.sh --app 28au74d9cj --update items 1kx2-ab --data '{"done":true}' --if-rev 3
45
45
  bash execute.sh --app 28au74d9cj --add items --data '{"name":"eggs","done":false}'
46
46
  bash execute.sh --app 28au74d9cj --delete items 1kx2-ab
47
+ bash execute.sh --app 28au74d9cj --request-access --reason "write the morning briefing" # ask the owner, see "Collaborators"
48
+ bash execute.sh --app 28au74d9cj --collaborators # who the owner let in
49
+ bash execute.sh --app 28au74d9cj --owner # who the app's comments go to
50
+ bash execute.sh --app 28au74d9cj --set-owner 'channel:#daily-brief' # see "Owner"
51
+ bash execute.sh --app 28au74d9cj --add-collaborator Kai # owner agents only
47
52
  ```
48
53
 
49
54
  Output:
@@ -53,8 +58,9 @@ Output:
53
58
  {"success":true,"id":"settings","data":{"title":"Weekly shop"},"rev":1,"updatedAt":"…"}
54
59
  ```
55
60
 
56
- - You can use the data of apps you published or a teammate published
57
- (`not_your_app` otherwise).
61
+ - You can use the data of apps you published, that a teammate published, or
62
+ that the owner added you (or your team) to as a collaborator (`not_your_app`
63
+ otherwise).
58
64
  - `--data-file <path>` reads the JSON from a regular file inside your project
59
65
  directory (not a symlink, not under `~/.crewly`, at most 1 MB).
60
66
  - `--list` pages with `--after <next>` until `next` is null.
@@ -64,6 +70,62 @@ Output:
64
70
  - The app's page sees your writes at once (its subscriptions fire). Your
65
71
  writes never wake you; the owner's do (`[APP CHANGES]`).
66
72
 
73
+ ## Collaborators: working in an app another team published
74
+
75
+ If the app is another team's, you get `not_your_app`. Do not publish a copy.
76
+ Ask the owner:
77
+
78
+ ```bash
79
+ bash execute.sh --app <appId> --request-access --reason "what you need to write"
80
+ ```
81
+
82
+ - The owner gets one card, **Allow / Do not allow**. Only their tap grants
83
+ anything; you cannot add yourself, and nothing you send changes who is added
84
+ (it is your own team, or only you with `--scope agent`).
85
+ - You are told when they answer. With **Allow** your team (and teammates who join
86
+ later) can use that app's **data and comments** with this skill, using the app
87
+ id. You get the app id from the owner or the publisher; collaborators do not
88
+ show up in `publish-app`'s list.
89
+ - It is **data only**: you cannot republish, roll back, transfer or change who
90
+ can open the app (`publish-app --app <id>` is refused). To change the page
91
+ itself, ask the publisher.
92
+ - The owner can take the access away at any time; your next call then returns
93
+ `not_your_app`.
94
+ - `--list` shows who wrote each document: documents written by an agent carry
95
+ `by` (its session), e.g. `crewly-marketing-ella-…`.
96
+
97
+ ## Owner: who the app's comments go to
98
+
99
+ Every app has an **owner**: one agent, a team, or a Crewly channel (a
100
+ cross-team room, see `list-channels`). By default it is the agent that
101
+ published it. The owner's comments in the app go to the owner:
102
+
103
+ - agent → that agent, mirrored to its Slack DM with the owner (as before);
104
+ - team → the team's room (its Slack team channel); the room's rules decide
105
+ who answers (an @mention in the comment → that agent; otherwise whoever is
106
+ awake reads it, and the lead when nobody is);
107
+ - channel → that channel, so its members from every team see the owner's
108
+ comments and any of them can pick one up.
109
+
110
+ ```bash
111
+ bash execute.sh --app <appId> --owner # {"owner":{"kind":"agent","name":"Ella","default":true}}
112
+ bash execute.sh --app <appId> --set-owner 'channel:#daily-brief' # or 'team:Dev', 'agent:Kai', default
113
+ bash execute.sh --app <appId> --add-collaborator Kai # let another agent of this machine use the data
114
+ ```
115
+
116
+ - Only the app's **owner agents** may change the owner or add a collaborator:
117
+ the owning agent, or a member of the owning team / channel (by default the
118
+ publisher). Anyone else gets `forbidden`; ask the owner, or use
119
+ `--request-access` for yourself.
120
+ - Members of the owning team or channel can use the app's data and comments
121
+ without being added. A collaborator gets data and comments only, never
122
+ publishing (that stays with the publisher's team).
123
+ - In a room, answer in the comment's thread: your reply there is added to the
124
+ comment in the app (do not also use `app-comments --reply`). Resolve with
125
+ `app-comments --resolve <id>` when it is done.
126
+ - The channel or team must exist on this machine (a channel made a moment ago
127
+ is fine).
128
+
67
129
  ## What you read is a sanitised display copy
68
130
 
69
131
  Every string you read here — values **and** object keys, at any depth — is
@@ -19,6 +19,11 @@ 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> --request-access [--scope team|agent] [--reason "why"]
23
+ bash execute.sh --app <appId> --collaborators
24
+ bash execute.sh --app <appId> --owner
25
+ bash execute.sh --app <appId> --set-owner 'channel:#daily-brief' # or 'team:<name>', 'agent:<name>', default
26
+ bash execute.sh --app <appId> --add-collaborator <agent name or session>
22
27
 
23
28
  Options:
24
29
  --app App id (from publish-app)
@@ -26,6 +31,13 @@ Options:
26
31
  --if-rev Only update when the doc is still at this rev (409 conflict otherwise)
27
32
  --limit Page size for --list (1-500, default 100)
28
33
  --after Continue a --list after this doc id (the previous page's "next")
34
+ --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
+ --collaborators Who the owner let work in the app
36
+ --scope team (your team, default) or agent (only you), with --request-access
37
+ --reason One line for the owner, with --request-access
38
+ --owner Show who owns the app's comments (an agent, a team or a channel)
39
+ --set-owner Change it: channel:#<name>, team:<name>, agent:<name> or default (the publishing agent). Only the app's owner agents (and the owner) may
40
+ --add-collaborator An owner agent lets another agent of this machine work in the app's data
29
41
  --help | -h Show this help
30
42
  EOF_USAGE
31
43
  }
@@ -69,7 +81,7 @@ process.stdout.write(fs.readFileSync(r, 'utf8'));
69
81
  NODE
70
82
  }
71
83
 
72
- APP=""; OP=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""
84
+ APP=""; OP=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""; SCOPE=""; REASON=""; SET_OWNER=""; WHO=""
73
85
  while [[ $# -gt 0 ]]; do
74
86
  case "$1" in
75
87
  --app) [ $# -ge 2 ] || error_exit "--app requires a value"; APP="$2"; shift 2 ;;
@@ -84,18 +96,54 @@ while [[ $# -gt 0 ]]; do
84
96
  --if-rev) [ $# -ge 2 ] || error_exit "--if-rev requires a value"; IF_REV="$2"; shift 2 ;;
85
97
  --limit) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
86
98
  --after) [ $# -ge 2 ] || error_exit "--after requires a value"; AFTER="$2"; shift 2 ;;
99
+ --request-access) OP="request-access"; shift ;;
100
+ --collaborators) OP="collaborators"; shift ;;
101
+ --owner) OP="owner"; shift ;;
102
+ --set-owner) [ $# -ge 2 ] || error_exit "--set-owner requires an owner (channel:#name, team:<name>, agent:<name> or default)"; OP="owner"; SET_OWNER="$2"; shift 2 ;;
103
+ --add-collaborator) [ $# -ge 2 ] || error_exit "--add-collaborator requires an agent name or session"; OP="add-collaborator"; WHO="$2"; shift 2 ;;
104
+ --scope) [ $# -ge 2 ] || error_exit "--scope requires a value"; SCOPE="$2"; shift 2 ;;
105
+ --reason) [ $# -ge 2 ] || error_exit "--reason requires a value"; REASON="$2"; shift 2 ;;
87
106
  --help|-h) print_usage; exit 0 ;;
88
107
  *) error_exit "Unknown option: $1" ;;
89
108
  esac
90
109
  done
91
110
 
92
111
  [ -n "$APP" ] || error_exit "--app is required"
93
- [ -n "$OP" ] || error_exit "one of --list / --get / --set / --update / --add / --delete is required"
112
+ [ -n "$OP" ] || error_exit "one of --list / --get / --set / --update / --add / --delete / --request-access / --collaborators / --owner / --add-collaborator is required"
94
113
  [[ "$APP" =~ ^[a-z0-9]{10}$ ]] || error_exit "--app must be a 10-character app id"
95
114
  case "$COLL" in .|..) error_exit "collection cannot be . or .." ;; esac
96
115
  case "$DOC" in .|..) error_exit "doc id cannot be . or .." ;; esac
97
116
  BASE="/apps/${APP}/data/$(uri "$COLL")"
98
117
 
118
+ case "$OP" in
119
+ request-access)
120
+ case "$SCOPE" in ""|team|agent) ;; *) error_exit "--scope is team or agent" ;; esac
121
+ BODY=$(jq -cn --arg s "$SCOPE" --arg r "$REASON" '{} + (if $s != "" then {scope: $s} else {} end) + (if $r != "" then {reason: $r} else {} end)')
122
+ RESPONSE=$(call POST "/apps/${APP}/collaborators/request" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
123
+ printf '%s' "$RESPONSE" | jq -c '{success: true, requested: .data.requested, for: .data.for, note: "The owner got a card. You are told here when they answer; until then you still cannot use this app."}'
124
+ exit 0
125
+ ;;
126
+ collaborators)
127
+ RESPONSE=$(call GET "/apps/${APP}/collaborators") || { printf '%s\n' "$RESPONSE"; exit 1; }
128
+ printf '%s' "$RESPONSE" | jq -c '{success: true, collaborators: [.data.collaborators[]? | {id, kind, name, who}]}'
129
+ exit 0
130
+ ;;
131
+ owner)
132
+ if [ -n "$SET_OWNER" ]; then
133
+ RESPONSE=$(call PUT "/apps/${APP}/owner" "$(jq -cn --arg o "$SET_OWNER" '{owner: $o}')") || { printf '%s\n' "$RESPONSE"; exit 1; }
134
+ else
135
+ RESPONSE=$(call GET "/apps/${APP}/owner") || { printf '%s\n' "$RESPONSE"; exit 1; }
136
+ fi
137
+ printf '%s' "$RESPONSE" | jq -c '{success: true, owner: (.data.owner | if . == null then null else {kind, name, default: (.explicit | not)} + (if .members then {members: .members} else {} end) end)}'
138
+ exit 0
139
+ ;;
140
+ add-collaborator)
141
+ RESPONSE=$(call POST "/apps/${APP}/collaborators/agents" "$(jq -cn --arg a "$WHO" '{agent: $a}')") || { printf '%s\n' "$RESPONSE"; exit 1; }
142
+ printf '%s' "$RESPONSE" | jq -c '{success: true, collaborators: [.data.collaborators[]? | {id, kind, name, who}]}'
143
+ exit 0
144
+ ;;
145
+ esac
146
+
99
147
  need_data() {
100
148
  [ -n "$DATA" ] || error_exit "--data is required for --$OP"
101
149
  printf '%s' "$DATA" | jq -e 'type == "object"' >/dev/null 2>&1 || error_exit "--data must be a JSON object"
@@ -107,7 +155,7 @@ case "$OP" in
107
155
  [ -n "$LIMIT" ] && Q="limit=$(uri "$LIMIT")"
108
156
  [ -n "$AFTER" ] && Q="${Q:+$Q&}after=$(uri "$AFTER")"
109
157
  RESPONSE=$(call GET "${BASE}${Q:+?$Q}") || { printf '%s\n' "$RESPONSE"; exit 1; }
110
- printf '%s' "$RESPONSE" | jq -c '{success: true, docs: [.data.docs[]? | {id, data, rev, updatedAt, updatedBy: (.updatedBy.kind // null)}], next: .data.next}'
158
+ printf '%s' "$RESPONSE" | jq -c '{success: true, docs: [.data.docs[]? | {id, data, rev, updatedAt, updatedBy: (.updatedBy.kind // null)} + (if .updatedBy.kind == "agent" then {by: .updatedBy.id} else {} end)], next: .data.next}'
111
159
  ;;
112
160
  get)
113
161
  RESPONSE=$(call GET "${BASE}/$(uri "$DOC")") || { printf '%s\n' "$RESPONSE"; exit 1; }
@@ -26,6 +26,18 @@ class H(BaseHTTPRequestHandler):
26
26
  self.wfile.write(json.dumps(obj).encode())
27
27
  if self.path.endswith('/missing'):
28
28
  return send(404, {'success': False, 'error': 'not_found', 'message': 'Document not found.', 'hint': 'No such app, document or version for this account.'})
29
+ if self.path.endswith('/collaborators') and method == 'GET':
30
+ return send(200, {'success': True, 'data': {'collaborators': [{'id': 'e1', 'kind': 'team', 'who': 'Marketing', 'name': 'Marketing', 'instanceId': 'i'}]}})
31
+ if self.path.endswith('/owner'):
32
+ if method == 'PUT':
33
+ return send(200, {'success': True, 'data': {'appId': 'x', 'owner': {'kind': 'channel', 'explicit': True, 'channelId': 'h1', 'name': '#daily-brief', 'instanceId': 'i', 'members': ['a', 'b']}, 'previous': None}})
34
+ return send(200, {'success': True, 'data': {'appId': 'x', 'owner': {'kind': 'agent', 'explicit': False, 'session': 'dev-ella', 'name': 'Ella', 'instanceId': 'i'}}})
35
+ if self.path.endswith('/collaborators/agents'):
36
+ return send(200, {'success': True, 'data': {'collaborators': [{'id': 'e2', 'kind': 'agent', 'who': 'crewly-dev-kai-1', 'name': 'Kai', 'instanceId': 'i'}]}})
37
+ if self.path.endswith('/collaborators/request'):
38
+ return send(200, {'success': True, 'data': {'requested': True, 'decisionId': 'D-1', 'for': 'the Marketing team'}})
39
+ if self.path.endswith('/byagent'):
40
+ return send(200, {'success': True, 'data': {'docs': [{'id': 'a', 'data': {}, 'rev': 1, 'updatedAt': 't', 'updatedBy': {'kind': 'agent', 'id': 'crewly-marketing-ella-1'}}], 'next': None}})
29
41
  if (data or {}).get('ifRev') == 1:
30
42
  return send(409, {'success': False, 'error': 'conflict', 'message': 'rev mismatch'})
31
43
  if method == 'GET' and '?' in self.path or self.path.endswith('/items'):
@@ -104,5 +116,35 @@ check "data-file in Crewly home refused" "$(printf '%s' "$OUT" | grep -c "inside
104
116
  OUT=$(run_err --app $A --get items .. || true)
105
117
  check "doc id .. refused" "$(printf '%s' "$OUT" | grep -c 'cannot be')" "1"
106
118
 
119
+ # A write by an agent shows who (the session); an owner write does not carry an id.
120
+ OUT=$(run --app $A --list byagent)
121
+ check "list: agent write shows its session" "$(printf '%s' "$OUT" | jq -c '.docs[0] | {updatedBy, by}')" '{"updatedBy":"agent","by":"crewly-marketing-ella-1"}'
122
+
123
+ # Asking for access: POST .../collaborators/request with the scope and reason only.
124
+ OUT=$(run --app $A --request-access --scope agent --reason "write the briefing")
125
+ check "request-access: path" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "POST /api/apps/$A/collaborators/request"
126
+ check "request-access: body" "$(jq -c .body "$STUB_LOG")" '{"scope":"agent","reason":"write the briefing"}'
127
+ check "request-access: output says requested" "$(printf '%s' "$OUT" | jq -c '{success, requested, for}')" '{"success":true,"requested":true,"for":"the Marketing team"}'
128
+ OUT=$(run_err --app $A --request-access --scope everyone || true)
129
+ check "request-access: bad scope refused" "$(printf '%s' "$OUT" | grep -c 'team or agent')" "1"
130
+ OUT=$(run --app $A --collaborators)
131
+ check "collaborators: output" "$OUT" '{"success":true,"collaborators":[{"id":"e1","kind":"team","name":"Marketing","who":"Marketing"}]}'
132
+
133
+ # App owner (crewly-services apps/SPEC.md §15).
134
+ OUT=$(run --app $A --owner)
135
+ check "owner: shows the default owner" "$OUT" '{"success":true,"owner":{"kind":"agent","name":"Ella","default":true}}'
136
+ check "owner: GET path" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "GET /api/apps/$A/owner"
137
+ OUT=$(run --app $A --set-owner 'channel:#daily-brief')
138
+ check "set-owner: request" "$(jq -c '{method, path, body}' "$STUB_LOG")" "{\"method\":\"PUT\",\"path\":\"/api/apps/$A/owner\",\"body\":{\"owner\":\"channel:#daily-brief\"}}"
139
+ check "set-owner: output" "$OUT" '{"success":true,"owner":{"kind":"channel","name":"#daily-brief","default":false,"members":["a","b"]}}'
140
+ OUT=$(run --app $A --add-collaborator Kai)
141
+ check "add-collaborator: request" "$(jq -c '{method, path, body}' "$STUB_LOG")" "{\"method\":\"POST\",\"path\":\"/api/apps/$A/collaborators/agents\",\"body\":{\"agent\":\"Kai\"}}"
142
+ check "add-collaborator: output" "$(printf '%s' "$OUT" | jq -c '.collaborators[0].name')" '"Kai"'
143
+ OUT=$(run_err --app $A --set-owner || true)
144
+ check "set-owner needs a value" "$(printf '%s' "$OUT" | grep -c 'requires an owner')" "1"
145
+ # --set still means a data write.
146
+ run --app $A --set items milk --data '{"done":true}' >/dev/null
147
+ check "--set is still a data write" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "PUT /api/apps/$A/data/items/milk"
148
+
107
149
  echo "app-data: $PASS passed, $FAIL failed"
108
150
  [ "$FAIL" -eq 0 ]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Publish App
3
3
  description: Publish a small web app (HTML/JS) you wrote to https://apps.crewlyai.com/<appId> so the owner can open it on their phone. Creates the app the first time, uploads a new version each time after (same app for the same directory), supports rollback, and can post an "Open app" card to the owner (a one-tap signed link, in your DM with them). Can ask the owner to make an app public (only the owner can approve it). The owner's edits, comments on elements, and anonymous visitors' submissions on a public app, come back to you as 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:
@@ -48,6 +48,8 @@ bash execute.sh --html ./timer.html --name "Timer"
48
48
  bash execute.sh --app 28au74d9cj --rollback 2 # back to version 2
49
49
  bash execute.sh --app 28au74d9cj --versions
50
50
  bash execute.sh --app 28au74d9cj --transfer-to edu-game-milo-13e8d3ca # hand the app to another agent / team
51
+ bash execute.sh --dir ./daily-brief --owner 'channel:#daily-brief' # publish, and send the owner's comments to a channel
52
+ bash execute.sh --app 28au74d9cj --owner 'team:Dev' # change only who gets the comments
51
53
  bash execute.sh --list # apps published from this machine
52
54
  bash execute.sh --app 28au74d9cj --share # send the owner a fresh one-tap link (no publish)
53
55
  bash execute.sh --app 28au74d9cj --links # its open-links; --revoke-link <id> / --revoke-links
@@ -132,6 +134,8 @@ bash execute.sh --app 28au74d9cj --private # pr
132
134
  shared keep working. So test a new version before publishing it to a public
133
135
  app. Widening what is public (more `--public-read` / `--public-submit`
134
136
  collections) still needs a new request the owner approves.
137
+ - Collaborators: an app another team published is not yours to republish. `--app <id>` from a team that did not publish it is refused (`not_your_app`), including when the owner added your team as a collaborator: collaborators get the app's **data and comments** only (use `app-data`; ask with `app-data --app <id> --request-access`). To change the page itself, ask the publisher or the owner to transfer the app (`--transfer-to`).
138
+ - Owner: `--owner channel:#<name> | team:<name> | agent:<name> | default` sets who the owner's **comments** go to (a channel's members from every team see them; see `app-data` → Owner). It does not change who publishes. Only the app's owner agents may change it (by default the publisher); with a publish the owner is set right after it, and an `ownerError` in the output means only that part failed.
135
139
  - Transfer: `--app <appId> --transfer-to <session>` hands the app to another agent, e.g. when the owner moves the work to a new team. Only the app's publisher, the lead of its team, the orchestrator or the owner may do it, and the target must be a member of an active team on this machine. Afterwards the new publisher publishes with `--app <appId> --dir <its directory>`, comments and changes go to it, and the old team can no longer publish, roll back or use the app's data. Both agents get a short note.
136
140
  - Thumbnails: the owner's portal list shows a small screenshot of each app. Publishing (and rollback) takes it automatically in the background when this machine has Chrome or Chromium; `bash execute.sh --app <appId> --refresh-thumbnail` re-takes it now (answers `captured:false` with a reason such as `no_browser` when it cannot).
137
141
  - `--private` is always allowed and takes effect at once. Use it if anything
@@ -29,6 +29,7 @@ Usage:
29
29
  bash execute.sh --app <appId> --rollback <version> # make an earlier version current
30
30
  bash execute.sh --app <appId> --versions # list versions
31
31
  bash execute.sh --app <appId> --transfer-to <session> # hand the app to another agent / team
32
+ bash execute.sh --app <appId> --owner 'channel:#daily-brief' # who gets the owner's comments (also with a publish)
32
33
  bash execute.sh --list # apps published from this machine
33
34
  bash execute.sh --app <appId> --share [--ttl-days 7] # fresh one-tap link card to the owner (no publish)
34
35
  bash execute.sh --app <appId> --links # open-links of the app (no secrets)
@@ -63,6 +64,8 @@ Options:
63
64
  --rollback Version number to make current (needs --app)
64
65
  --versions List versions (needs --app)
65
66
  --transfer-to Hand the app to another agent (its session name; it must be on a team on this machine).
67
+ --owner Who owns the app's comments: channel:#<name>, team:<name>, agent:<name> or default (the publishing agent).
68
+ With a publish it is set after the publish; alone it needs --app. Only the app's owner agents may change it.
66
69
  Allowed for the app's publisher, the lead of its team, the orchestrator or the owner.
67
70
  The new publisher publishes with --app <appId> --dir <its directory>; the old team loses access.
68
71
  --list List apps published from this machine
@@ -84,7 +87,7 @@ call() {
84
87
  }
85
88
 
86
89
  DIR=""; FILE=""; NAME=""; APP=""; ENTRY=""; NOTE=""; NOTIFY=false; ROLLBACK=""; TRANSFER_TO=""; VERSIONS=false; LIST=false
87
- SHARE=false; TTL_DAYS=""; LINKS=false; REVOKE_LINK=""; REVOKE_LINKS=false
90
+ SHARE=false; TTL_DAYS=""; LINKS=false; REVOKE_LINK=""; REVOKE_LINKS=false; OWNER_SPEC=""
88
91
  PUBLIC=false; PUBLIC_READ=""; PUBLIC_SUBMIT=""; PUBLIC_NOTE=""; CANCEL_PUBLIC=false; PRIVATE=false; REFRESH_THUMB=false
89
92
  while [[ $# -gt 0 ]]; do
90
93
  case "$1" in
@@ -110,6 +113,7 @@ while [[ $# -gt 0 ]]; do
110
113
  --refresh-thumbnail) REFRESH_THUMB=true; shift ;;
111
114
  --versions) VERSIONS=true; shift ;;
112
115
  --transfer-to) [ $# -ge 2 ] || error_exit "--transfer-to requires a session name"; TRANSFER_TO="$2"; shift 2 ;;
116
+ --owner) [ $# -ge 2 ] || error_exit "--owner requires channel:#<name>, team:<name>, agent:<name> or default"; OWNER_SPEC="$2"; shift 2 ;;
113
117
  --list) LIST=true; shift ;;
114
118
  --help|-h) print_usage; exit 0 ;;
115
119
  *) error_exit "Unknown option: $1" ;;
@@ -201,6 +205,15 @@ if [ -z "$DIR" ] && [ -z "$FILE" ]; then
201
205
  fi
202
206
  fi
203
207
 
208
+ # The app's owner (crewly-services apps/SPEC.md §15): who its comments go to.
209
+ OWNER_JQ='(.data.owner | if . == null then null else {kind, name, default: (.explicit | not)} end)'
210
+ if [ -n "$OWNER_SPEC" ] && [ -z "$DIR" ] && [ -z "$FILE" ]; then
211
+ [ -n "$APP" ] || error_exit "--app is required with --owner (or publish with --dir / --html)"
212
+ RESPONSE=$(call PUT "/apps/${APP}/owner" "$(jq -cn --arg o "$OWNER_SPEC" '{owner: $o}')") || { printf '%s\n' "$RESPONSE"; exit 1; }
213
+ printf '%s' "$RESPONSE" | jq -c '{success: true, appId: .data.appId, owner: '"$OWNER_JQ"'}'
214
+ exit 0
215
+ fi
216
+
204
217
  if [ -n "$TRANSFER_TO" ]; then
205
218
  [ -n "$APP" ] || error_exit "--app is required with --transfer-to"
206
219
  BODY=$(jq -cn --arg s "$TRANSFER_TO" '{toSession: $s}')
@@ -311,9 +324,19 @@ OUT=$(curl -s -w '\n%{http_code}' -X POST -H "Content-Type: application/json" ${
311
324
  CODE=$(printf '%s\n' "$OUT" | tail -n 1)
312
325
  RESP=$(printf '%s\n' "$OUT" | sed '$d')
313
326
  if [ "$CODE" -ge 200 ] 2>/dev/null && [ "$CODE" -lt 300 ] 2>/dev/null; then
314
- printf '%s' "$RESP" | jq -c --arg msg "$PUBLIC_MSG" '.data | {success: true, appId, name, url: ("https://apps.crewlyai.com/" + .appId), version, created, notified} + '"$CARD_JQ"'
327
+ OWNER_OUT='{}'
328
+ if [ -n "$OWNER_SPEC" ]; then
329
+ NEW_APP=$(printf '%s' "$RESP" | jq -r '.data.appId // empty')
330
+ if OWNER_RESP=$(call PUT "/apps/${NEW_APP}/owner" "$(jq -cn --arg o "$OWNER_SPEC" '{owner: $o}')"); then
331
+ OWNER_OUT=$(printf '%s' "$OWNER_RESP" | jq -c '{owner: '"$OWNER_JQ"'}')
332
+ else
333
+ # The publish went through; only the owner change failed.
334
+ OWNER_OUT=$(printf '%s' "$OWNER_RESP" | jq -c '{ownerError: (.message // .reason // "failed")}' 2>/dev/null || echo '{"ownerError":"failed"}')
335
+ fi
336
+ fi
337
+ printf '%s' "$RESP" | jq -c --arg msg "$PUBLIC_MSG" --argjson own "$OWNER_OUT" '.data | {success: true, appId, name, url: ("https://apps.crewlyai.com/" + .appId), version, created, notified} + '"$CARD_JQ"'
315
338
  + (if .publicRequested == true then {publicRequested: true, message: $msg} elif .publicRequested == false then {publicRequested: false, publicError} else {} end)
316
- + (if .publicPaused == true then {publicPaused: true, publicPausedMessage} else {} end)'
339
+ + (if .publicPaused == true then {publicPaused: true, publicPausedMessage} else {} end) + $own'
317
340
  else
318
341
  printf '%s' "$RESP" | jq -c --arg code "$CODE" '{success: false, status: ($code|tonumber), reason: (.error // "unknown"), message: (.message // ""), hint: (.hint // "")}' 2>/dev/null \
319
342
  || jq -cn --arg code "$CODE" '{success: false, status: ($code|tonumber? // 0), reason: "http_error"}'
@@ -63,6 +63,10 @@ class H(BaseHTTPRequestHandler):
63
63
  return send(200, {'success': True, 'data': {'appId': 'bcdfghjkmn', 'captured': False, 'reason': 'no_browser', 'message': 'No Chrome'}})
64
64
  if self.path == '/api/apps/28au74d9cj/make-private':
65
65
  return send(200, {'success': True, 'data': {'appId': '28au74d9cj', 'visibility': 'private'}})
66
+ if self.path.endswith('/owner') and method == 'PUT':
67
+ if data.get('owner') == 'channel:#nope':
68
+ return send(400, {'success': False, 'error': 'validation', 'message': 'No channel "#nope" on this machine.'})
69
+ return send(200, {'success': True, 'data': {'appId': '28au74d9cj', 'owner': {'kind': 'channel', 'explicit': True, 'name': '#daily-brief'}, 'previous': None}})
66
70
  if self.path == '/api/apps/28au74d9cj/transfer':
67
71
  return send(200, {'success': True, 'data': {'appId': '28au74d9cj', 'name': 'G', 'publisher': data['toSession'], 'previous': 'dev-ella', 'changed': True, 'notified': [data['toSession']]}})
68
72
  if self.path.endswith('/rollback'):
@@ -79,6 +83,9 @@ class H(BaseHTTPRequestHandler):
79
83
  def do_POST(self):
80
84
  n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
81
85
  self._reply('POST', json.loads(body or '{}'))
86
+ def do_PUT(self):
87
+ n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
88
+ self._reply('PUT', json.loads(body or '{}'))
82
89
  def log_message(self, *a, **k): pass
83
90
  HTTPServer(('127.0.0.1', PORT), H).serve_forever()
84
91
  PY
@@ -220,5 +227,16 @@ check "refresh-thumbnail without a browser" "$OUT" '{"success":false,"appId":"bc
220
227
  OUT=$(run_err --dir "$APPDIR" --refresh-thumbnail || true)
221
228
  check "--refresh-thumbnail is not a publish option" "$(printf '%s' "$OUT" | grep -c 'act on an app')" "1"
222
229
 
230
+ # --owner (crewly-services apps/SPEC.md §15)
231
+ OUT=$(run --app 28au74d9cj --owner 'channel:#daily-brief')
232
+ check "owner alone: output" "$OUT" '{"success":true,"appId":"28au74d9cj","owner":{"kind":"channel","name":"#daily-brief","default":false}}'
233
+ check "owner alone: request" "$(jq -c '{method, path, body}' "$STUB_LOG")" '{"method":"PUT","path":"/api/apps/28au74d9cj/owner","body":{"owner":"channel:#daily-brief"}}'
234
+ check "owner alone needs --app" "$(run_err --owner 'team:dev' | grep -c -- '--app is required with --owner')" "1"
235
+ OUT=$(run --dir "$APPDIR" --owner 'channel:#daily-brief')
236
+ check "publish + owner: set after the publish" "$(printf '%s' "$OUT" | jq -c '{success, owner}')" '{"success":true,"owner":{"kind":"channel","name":"#daily-brief","default":false}}'
237
+ check "publish + owner: last request is the owner change" "$(jq -r '.method + " " + .path' "$STUB_LOG")" "PUT /api/apps/28au74d9cj/owner"
238
+ OUT=$(run --dir "$APPDIR" --owner 'channel:#nope')
239
+ check "publish + bad owner: publish still succeeds, ownerError says why" "$(printf '%s' "$OUT" | jq -c '{success, ownerError}')" '{"success":true,"ownerError":"No channel \"#nope\" on this machine."}'
240
+
223
241
  echo "publish-app: $PASS passed, $FAIL failed"
224
242
  [ "$FAIL" -eq 0 ]
@@ -4727,6 +4727,38 @@ export declare const CREWLY_APPS_CONSTANTS: {
4727
4727
  /** Anchor fields shown in a wake */
4728
4728
  readonly MAX_WAKE_SELECTOR_CHARS: 200;
4729
4729
  readonly MAX_WAKE_TEXT_CHARS: 120;
4730
+ /**
4731
+ * A comment for an app owned by a team or channel (crewly-services
4732
+ * apps/SPEC.md §15) is posted into that room after this short window (no
4733
+ * cooldown: each comment is its own thread there, and the room's rules
4734
+ * decide who is woken).
4735
+ */
4736
+ readonly ROOM_BATCH_WINDOW_MS: 5000;
4737
+ /** Owner text shown in the room's (and its Slack channel's) thread */
4738
+ readonly MAX_ROOM_BODY_CHARS: 2000;
4739
+ /** Sender label of the owner's comment rows in a room */
4740
+ readonly ROOM_SENDER: "Owner (app comment)";
4741
+ /** chat-v2 `metadata.source` of rows written for app comments in a room */
4742
+ readonly ROOM_SOURCE: "app-comment";
4743
+ };
4744
+ /**
4745
+ * Voice comments (crewly-services apps/SPEC.md §16): the owner records audio
4746
+ * in the app's comment box; this instance downloads it when the comment is
4747
+ * delivered and tells the agent the local path to transcribe.
4748
+ */
4749
+ readonly VOICE: {
4750
+ /** Where recordings are saved (relative to the Crewly home), next to Slack's downloaded files */
4751
+ readonly DIR: "tmp/app-comment-audio";
4752
+ /** Largest recording accepted (Cloud's cap is 5 MB; a little slack) */
4753
+ readonly MAX_BYTES: number;
4754
+ /** One download */
4755
+ readonly DOWNLOAD_TIMEOUT_MS: 30000;
4756
+ /** Recordings fetched for one wake (the rest are named with the command to fetch them) */
4757
+ readonly MAX_PER_WAKE: 10;
4758
+ /** Recording ids Cloud issues */
4759
+ readonly BLOB_ID_PATTERN: RegExp;
4760
+ /** File extension per audio type */
4761
+ readonly EXT: Readonly<Record<string, string>>;
4730
4762
  };
4731
4763
  /**
4732
4764
  * @mentions in comments (crewly-services apps/SPEC.md §12.1): this
@@ -4739,6 +4771,10 @@ export declare const CREWLY_APPS_CONSTANTS: {
4739
4771
  readonly MAX_AGENTS: 200;
4740
4772
  readonly MAX_NAME_CHARS: 60;
4741
4773
  readonly MAX_TEAM_CHARS: 80;
4774
+ /** Teams / channels / members per channel pushed with the roster (Cloud's caps; app owners, SPEC §15) */
4775
+ readonly MAX_TEAMS: 100;
4776
+ readonly MAX_CHANNELS: 100;
4777
+ readonly MAX_CHANNEL_MEMBERS: 100;
4742
4778
  /** An unchanged roster is re-pushed this often (Cloud drops one silent for 14 days) */
4743
4779
  readonly REFRESH_MS: number;
4744
4780
  };