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.
- package/config/skills/agent/core/app-comments/SKILL.md +25 -1
- package/config/skills/agent/core/app-comments/execute.sh +12 -3
- package/config/skills/agent/core/app-comments/execute.test.sh +17 -0
- package/config/skills/agent/core/app-data/SKILL.md +65 -3
- package/config/skills/agent/core/app-data/execute.sh +51 -3
- package/config/skills/agent/core/app-data/execute.test.sh +42 -0
- package/config/skills/agent/core/publish-app/SKILL.md +5 -1
- package/config/skills/agent/core/publish-app/execute.sh +26 -3
- package/config/skills/agent/core/publish-app/execute.test.sh +18 -0
- package/dist/backend/backend/src/constants.d.ts +36 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +44 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +24 -0
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.js +47 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +8 -0
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.js +31 -2
- package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +9 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/apps/app-collaborators.service.d.ts +103 -0
- package/dist/backend/backend/src/services/apps/app-collaborators.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/apps/app-collaborators.service.js +161 -0
- package/dist/backend/backend/src/services/apps/app-collaborators.service.js.map +1 -0
- package/dist/backend/backend/src/services/apps/app-comment-audio.service.d.ts +164 -0
- package/dist/backend/backend/src/services/apps/app-comment-audio.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/apps/app-comment-audio.service.js +217 -0
- package/dist/backend/backend/src/services/apps/app-comment-audio.service.js.map +1 -0
- package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts +156 -0
- package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/apps/app-comment-room.service.js +344 -0
- package/dist/backend/backend/src/services/apps/app-comment-room.service.js.map +1 -0
- package/dist/backend/backend/src/services/apps/app-comments-slack.service.d.ts +44 -2
- package/dist/backend/backend/src/services/apps/app-comments-slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/app-comments-slack.service.js +90 -10
- package/dist/backend/backend/src/services/apps/app-comments-slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/apps/app-roster.service.d.ts +37 -1
- package/dist/backend/backend/src/services/apps/app-roster.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/app-roster.service.js +62 -7
- package/dist/backend/backend/src/services/apps/app-roster.service.js.map +1 -1
- package/dist/backend/backend/src/services/apps/app-wake-message.d.ts +25 -1
- package/dist/backend/backend/src/services/apps/app-wake-message.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/app-wake-message.js +25 -13
- package/dist/backend/backend/src/services/apps/app-wake-message.js.map +1 -1
- package/dist/backend/backend/src/services/apps/app-wake.service.d.ts +34 -0
- package/dist/backend/backend/src/services/apps/app-wake.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/app-wake.service.js +97 -17
- package/dist/backend/backend/src/services/apps/app-wake.service.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts +25 -0
- package/dist/backend/backend/src/services/apps/apps-cloud.client.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/apps-cloud.client.js +41 -4
- package/dist/backend/backend/src/services/apps/apps-cloud.client.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.service.d.ts +115 -2
- package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.service.js +181 -9
- package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.wiring.d.ts +70 -1
- package/dist/backend/backend/src/services/apps/apps.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.wiring.js +274 -5
- package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +3 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts +2 -0
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.js +1 -0
- package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/backend/backend/src/types/decision.types.d.ts +27 -2
- package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +36 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +44 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +3 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts +2 -0
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.js +1 -0
- package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/cli/backend/src/types/decision.types.d.ts +27 -2
- package/dist/cli/backend/src/types/decision.types.d.ts.map +1 -1
- 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.
|
|
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.
|
|
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
|
|
57
|
-
(`not_your_app`
|
|
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
|
+
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
|
-
|
|
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
|
};
|