crewly 1.20.295 → 1.20.297
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/_common/lib.sh +6 -0
- package/config/skills/agent/core/app-data/SKILL.md +13 -1
- package/config/skills/agent/core/app-data/execute.sh +60 -6
- package/config/skills/agent/core/app-data/execute.test.sh +42 -0
- package/config/skills/agent/core/complete-task/SKILL.md +4 -2
- package/config/skills/agent/core/complete-task/execute.sh +7 -0
- package/config/skills/agent/core/complete-task/explicit-id.test.sh +56 -0
- package/config/skills/agent/core/docs-comment/SKILL.md +49 -4
- package/config/skills/agent/core/docs-comment/execute.sh +38 -2
- package/config/skills/agent/core/docs-comment/execute.test.sh +28 -0
- package/config/skills/agent/core/poll-tasks/execute.sh +13 -1
- package/config/skills/agent/core/poll-tasks/execute.test.sh +37 -0
- package/config/skills/team-leader/delegate-task/execute.sh +13 -2
- package/config/skills/team-leader/verify-output/SKILL.md +9 -0
- package/dist/backend/backend/src/constants.d.ts +39 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +39 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +2 -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 +2 -0
- package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +1 -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 +3 -1
- package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +20 -0
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +52 -9
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts +31 -0
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.controller.js +198 -5
- package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts +4 -0
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js +9 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +28 -8
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +28 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +35 -5
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +4 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.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-wake-message.d.ts +33 -0
- 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 +48 -0
- package/dist/backend/backend/src/services/apps/app-wake-message.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.service.d.ts +23 -0
- package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.service.js +57 -6
- package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
- package/dist/backend/backend/src/services/google/docs-comment-gate.d.ts +166 -0
- package/dist/backend/backend/src/services/google/docs-comment-gate.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/docs-comment-gate.js +176 -0
- package/dist/backend/backend/src/services/google/docs-comment-gate.js.map +1 -0
- package/dist/backend/backend/src/services/google/docs-comments.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/google/docs-comments.service.js +2 -2
- package/dist/backend/backend/src/services/google/docs-comments.service.js.map +1 -1
- package/dist/backend/backend/src/services/google/google-api.client.d.ts +15 -0
- package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
- package/dist/backend/backend/src/services/google/google-api.client.js +16 -2
- package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
- package/dist/backend/backend/src/services/google/google-reauth.wiring.js +1 -1
- package/dist/backend/backend/src/services/google/google-reauth.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts +12 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/google/google-workspace-token.service.js +18 -0
- package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +18 -4
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +36 -8
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts +6 -0
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.js +18 -0
- package/dist/backend/backend/src/services/orc/reply-destination-resolver.js.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts +2 -0
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +81 -8
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +8 -0
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js +22 -0
- package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts +17 -0
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js +32 -0
- package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/backend/backend/src/services/slack/orc-owner-dm-target.d.ts +27 -0
- package/dist/backend/backend/src/services/slack/orc-owner-dm-target.d.ts.map +1 -0
- package/dist/backend/backend/src/services/slack/orc-owner-dm-target.js +48 -0
- package/dist/backend/backend/src/services/slack/orc-owner-dm-target.js.map +1 -0
- package/dist/backend/backend/src/services/task-pool/pool-archive-migration.d.ts +18 -0
- package/dist/backend/backend/src/services/task-pool/pool-archive-migration.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/pool-archive-migration.js +9 -3
- package/dist/backend/backend/src/services/task-pool/pool-archive-migration.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/pool-prune.d.ts +76 -0
- package/dist/backend/backend/src/services/task-pool/pool-prune.d.ts.map +1 -0
- package/dist/backend/backend/src/services/task-pool/pool-prune.js +129 -0
- package/dist/backend/backend/src/services/task-pool/pool-prune.js.map +1 -0
- package/dist/backend/backend/src/services/task-pool/pool-storage.d.ts +9 -1
- package/dist/backend/backend/src/services/task-pool/pool-storage.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/pool-storage.js +34 -3
- package/dist/backend/backend/src/services/task-pool/pool-storage.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/runaway-file-watcher.d.ts +131 -0
- package/dist/backend/backend/src/services/worktree/runaway-file-watcher.d.ts.map +1 -0
- package/dist/backend/backend/src/services/worktree/runaway-file-watcher.js +251 -0
- package/dist/backend/backend/src/services/worktree/runaway-file-watcher.js.map +1 -0
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +30 -0
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +82 -0
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
- package/dist/backend/backend/src/utils/screen-excerpt.d.ts +34 -0
- package/dist/backend/backend/src/utils/screen-excerpt.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/screen-excerpt.js +56 -0
- package/dist/backend/backend/src/utils/screen-excerpt.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +39 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +39 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +4 -2
- package/dist/cli/backend/src/services/agent/agent-registration.service.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/messaging/sub-agent-message-queue.service.d.ts +18 -4
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +36 -8
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts +17 -0
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/session-command-helper.js +32 -0
- package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/pool-storage.d.ts +9 -1
- package/dist/cli/backend/src/services/task-pool/pool-storage.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/pool-storage.js +34 -3
- package/dist/cli/backend/src/services/task-pool/pool-storage.js.map +1 -1
- package/dist/cli/backend/src/utils/screen-excerpt.d.ts +34 -0
- package/dist/cli/backend/src/utils/screen-excerpt.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/screen-excerpt.js +56 -0
- package/dist/cli/backend/src/utils/screen-excerpt.js.map +1 -0
- package/package.json +1 -1
|
@@ -222,6 +222,12 @@ api_call() {
|
|
|
222
222
|
local url="${CREWLY_API_URL}/api${endpoint}"
|
|
223
223
|
_prune_skill_output
|
|
224
224
|
local args=(-s -w '\n%{http_code}' -X "$method")
|
|
225
|
+
# Optional ceiling on the whole request (seconds). A caller that can tell
|
|
226
|
+
# "the server is slow" from "it failed" (poll-tasks' claim) sets it; curl
|
|
227
|
+
# then exits 28 instead of hanging until the skill runner kills the call.
|
|
228
|
+
if [ -n "${CREWLY_API_MAX_TIME:-}" ]; then
|
|
229
|
+
args+=(--max-time "$CREWLY_API_MAX_TIME")
|
|
230
|
+
fi
|
|
225
231
|
# A raw file upload (app-data --upload): CREWLY_API_UPLOAD_FILE is sent as the
|
|
226
232
|
# body, byte for byte, with CREWLY_API_UPLOAD_TYPE as its Content-Type and the
|
|
227
233
|
# URI-encoded CREWLY_API_UPLOAD_NAME as X-File-Name. Everything else is JSON.
|
|
@@ -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.6.0
|
|
5
5
|
category: productivity
|
|
6
6
|
skillType: claude-skill
|
|
7
7
|
assignableRoles:
|
|
@@ -44,6 +44,7 @@ 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 --get-field items 1kx2-ab --field /photo --out ./photo.png --decode # full value of a cut field
|
|
47
48
|
bash execute.sh --app 28au74d9cj --request-access --reason "write the morning briefing" # ask the owner, see "Collaborators"
|
|
48
49
|
bash execute.sh --app 28au74d9cj --collaborators # who the owner let in
|
|
49
50
|
bash execute.sh --app 28au74d9cj --owner # who the app's comments go to
|
|
@@ -200,3 +201,14 @@ write private information there.
|
|
|
200
201
|
|
|
201
202
|
Collection names: `[A-Za-z0-9_-]{1,64}`. Doc ids: `[A-Za-z0-9_.:-]{1,128}`.
|
|
202
203
|
Documents are JSON objects up to 256 KB.
|
|
204
|
+
|
|
205
|
+
## Long values (over 65,536 characters)
|
|
206
|
+
|
|
207
|
+
`--get` and `--list` cut any string over 65,536 characters (a base64 photo, a
|
|
208
|
+
long document) so it cannot flood your terminal. A cut is never silent: the
|
|
209
|
+
answer carries `truncated: [{"field":"/photo","chars":123456}]` (in a list,
|
|
210
|
+
each entry also has the doc `id`). Do not parse a flagged value as if it were
|
|
211
|
+
whole. Fetch it with `--get-field <collection> <docId> --field <pointer> --out
|
|
212
|
+
<file>`: the full value goes to a file inside your project (nothing is printed
|
|
213
|
+
but `{file, bytes}`). Add `--decode` for base64 / data-URL values to get the
|
|
214
|
+
real binary (the image itself).
|
|
@@ -15,6 +15,7 @@ print_usage() {
|
|
|
15
15
|
Usage:
|
|
16
16
|
bash execute.sh --app <appId> --list <collection> [--limit 100] [--after <docId>]
|
|
17
17
|
bash execute.sh --app <appId> --get <collection> <docId>
|
|
18
|
+
bash execute.sh --app <appId> --get-field <collection> <docId> --field </json/pointer> --out <file> [--decode]
|
|
18
19
|
bash execute.sh --app <appId> --set <collection> <docId> --data '{"done":true}'
|
|
19
20
|
bash execute.sh --app <appId> --update <collection> <docId> --data '{"done":true}' [--if-rev 4]
|
|
20
21
|
bash execute.sh --app <appId> --add <collection> --data '{"name":"milk"}'
|
|
@@ -32,6 +33,10 @@ Options:
|
|
|
32
33
|
--if-rev Only update when the doc is still at this rev (409 conflict otherwise)
|
|
33
34
|
--limit Page size for --list (1-500, default 100)
|
|
34
35
|
--after Continue a --list after this doc id (the previous page's "next")
|
|
36
|
+
--get-field Save the FULL value of one field to a file (reads cut strings over 65,536 characters and list them in "truncated"); prints only {file, bytes}
|
|
37
|
+
--field JSON pointer into the doc's data, as listed in "truncated" (e.g. /photo, /items/0/image)
|
|
38
|
+
--out File to write for --get-field: inside your project directory, not an existing symlink
|
|
39
|
+
--decode With --get-field: base64-decode the value (a data:...;base64, prefix is dropped) so a photo is written as the real image
|
|
35
40
|
--upload Put a file (an image for a post) into the app and print its url; works for the owner's agents and for collaborators. Regular, non-symlink file inside your project directory, up to 10 MB (the app's own limit may be smaller)
|
|
36
41
|
--name File name to show for --upload (default: the file's name)
|
|
37
42
|
--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
|
|
@@ -51,10 +56,20 @@ fail_from() {
|
|
|
51
56
|
exit 1
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
# Run one API call. stdout (the whole response body, however many lines it
|
|
60
|
+
# spans) is kept apart from stderr (the missing-session warning, or the JSON
|
|
61
|
+
# error object api_call prints on failure) so neither pollutes the other.
|
|
54
62
|
call() {
|
|
55
|
-
local out
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
local out err errfile rc=0
|
|
64
|
+
errfile=$(mktemp "${TMPDIR:-/tmp}/app-data-err.XXXXXX") || errfile=/dev/null
|
|
65
|
+
out=$(api_call "$@" 2>"$errfile") || rc=$?
|
|
66
|
+
err=$(cat "$errfile" 2>/dev/null || true)
|
|
67
|
+
[ "$errfile" = /dev/null ] || rm -f "$errfile"
|
|
68
|
+
if [ "$rc" -ne 0 ]; then
|
|
69
|
+
# The error object is the last non-empty stderr line (warnings come first).
|
|
70
|
+
fail_from "$(printf '%s\n' "$err" | awk 'NF{l=$0} END{print l}')"
|
|
71
|
+
fi
|
|
72
|
+
printf '%s\n' "$out"
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
@@ -107,13 +122,35 @@ content_type_for() {
|
|
|
107
122
|
esac
|
|
108
123
|
}
|
|
109
124
|
|
|
110
|
-
|
|
125
|
+
# --out for --get-field: a new regular file inside the project directory
|
|
126
|
+
# (CREWLY_PROJECT_PATH, else the working directory), never under Crewly's home,
|
|
127
|
+
# never through a symlink. Prints the absolute target path.
|
|
128
|
+
resolve_out_path() {
|
|
129
|
+
local given="$1" dir base project real_dir
|
|
130
|
+
dir="$(dirname "$given")"; base="$(basename "$given")"
|
|
131
|
+
[ -d "$dir" ] || error_exit "the directory of --out does not exist: $dir"
|
|
132
|
+
real_dir="$(cd "$dir" && pwd -P)"
|
|
133
|
+
project="$(cd "${CREWLY_PROJECT_PATH:-$PWD}" && pwd -P)" || error_exit "cannot resolve the project directory"
|
|
134
|
+
case "$real_dir/" in "$project"/*) ;; *) error_exit "--out must be inside your project directory ($project)" ;; esac
|
|
135
|
+
case "$real_dir/" in "${CREWLY_HOME:-$HOME/.crewly}"/*|"$HOME/.crewly"/*) error_exit "--out is inside Crewly's home directory" ;; esac
|
|
136
|
+
[ ! -L "$real_dir/$base" ] || error_exit "--out is a symbolic link; pass a real file path"
|
|
137
|
+
[ ! -d "$real_dir/$base" ] || error_exit "--out is a directory"
|
|
138
|
+
printf '%s/%s' "$real_dir" "$base"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
APP=""; OP=""; UPLOAD=""; NAME=""; COLL=""; DOC=""; DATA=""; IF_REV=""; LIMIT=""; AFTER=""; SCOPE=""; REASON=""; SET_OWNER=""; WHO=""; FIELD=""; OUT_FILE=""; DECODE=""
|
|
111
142
|
while [[ $# -gt 0 ]]; do
|
|
112
143
|
case "$1" in
|
|
113
144
|
--app) [ $# -ge 2 ] || error_exit "--app requires a value"; APP="$2"; shift 2 ;;
|
|
114
145
|
--list|--add)
|
|
115
146
|
[ $# -ge 2 ] || error_exit "$1 requires a collection"
|
|
116
147
|
OP="${1#--}"; COLL="$2"; shift 2 ;;
|
|
148
|
+
--get-field)
|
|
149
|
+
[ $# -ge 3 ] || error_exit "--get-field requires a collection and a doc id"
|
|
150
|
+
OP="get-field"; COLL="$2"; DOC="$3"; shift 3 ;;
|
|
151
|
+
--field) [ $# -ge 2 ] || error_exit "--field requires a JSON pointer"; FIELD="$2"; shift 2 ;;
|
|
152
|
+
--out) [ $# -ge 2 ] || error_exit "--out requires a file path"; OUT_FILE="$2"; shift 2 ;;
|
|
153
|
+
--decode) DECODE=1; shift ;;
|
|
117
154
|
--get|--set|--update|--delete)
|
|
118
155
|
[ $# -ge 3 ] || error_exit "$1 requires a collection and a doc id"
|
|
119
156
|
OP="${1#--}"; COLL="$2"; DOC="$3"; shift 3 ;;
|
|
@@ -191,11 +228,28 @@ case "$OP" in
|
|
|
191
228
|
[ -n "$LIMIT" ] && Q="limit=$(uri "$LIMIT")"
|
|
192
229
|
[ -n "$AFTER" ] && Q="${Q:+$Q&}after=$(uri "$AFTER")"
|
|
193
230
|
RESPONSE=$(call GET "${BASE}${Q:+?$Q}") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
194
|
-
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}'
|
|
231
|
+
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} + (if .data.truncated then {truncated: .data.truncated, truncatedHint: .data.truncatedHint} else {} end)'
|
|
232
|
+
;;
|
|
233
|
+
get-field)
|
|
234
|
+
[ -n "$FIELD" ] || error_exit "--field is required for --get-field (a JSON pointer such as /photo)"
|
|
235
|
+
[ -n "$OUT_FILE" ] || error_exit "--out is required for --get-field"
|
|
236
|
+
TARGET="$(resolve_out_path "$OUT_FILE")" || exit 1
|
|
237
|
+
RESPONSE=$(CREWLY_SKILL_FULL_OUTPUT=1 call GET "${BASE}/$(uri "$DOC")/field?path=$(uri "$FIELD")") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
238
|
+
printf '%s' "$RESPONSE" | jq -e '.data.type == "string"' >/dev/null 2>&1 || error_exit "that field is not a string; read it with --get"
|
|
239
|
+
TMP_OUT="$(mktemp "${TARGET}.XXXXXX")" || error_exit "cannot write next to $OUT_FILE"
|
|
240
|
+
printf '%s' "$RESPONSE" | jq -j '.data.value' > "$TMP_OUT"
|
|
241
|
+
if [ -n "$DECODE" ]; then
|
|
242
|
+
sed -E '1s#^data:[^,]*;base64,##' "$TMP_OUT" | tr -d '\n\r ' > "${TMP_OUT}.b64"
|
|
243
|
+
{ base64 -d < "${TMP_OUT}.b64" > "${TMP_OUT}.bin" 2>/dev/null || base64 -D < "${TMP_OUT}.b64" > "${TMP_OUT}.bin" 2>/dev/null; } \
|
|
244
|
+
|| { rm -f "$TMP_OUT" "${TMP_OUT}.b64" "${TMP_OUT}.bin"; error_exit "the value is not valid base64"; }
|
|
245
|
+
mv "${TMP_OUT}.bin" "$TMP_OUT"; rm -f "${TMP_OUT}.b64"
|
|
246
|
+
fi
|
|
247
|
+
mv "$TMP_OUT" "$TARGET"
|
|
248
|
+
jq -cn --arg f "$TARGET" --argjson b "$(wc -c < "$TARGET" | tr -d ' ')" --argjson c "$(printf '%s' "$RESPONSE" | jq '.data.chars')" '{success: true, file: $f, bytes: $b, chars: $c}'
|
|
195
249
|
;;
|
|
196
250
|
get)
|
|
197
251
|
RESPONSE=$(call GET "${BASE}/$(uri "$DOC")") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
198
|
-
printf '%s' "$RESPONSE" | jq -c '{success: true, id: .data.id, data: .data.data, rev: .data.rev, updatedAt: .data.updatedAt}'
|
|
252
|
+
printf '%s' "$RESPONSE" | jq -c '{success: true, id: .data.id, data: .data.data, rev: .data.rev, updatedAt: .data.updatedAt} + (if .data.truncated then {truncated: .data.truncated, truncatedHint: .data.truncatedHint} else {} end)'
|
|
199
253
|
;;
|
|
200
254
|
set)
|
|
201
255
|
need_data
|
|
@@ -24,6 +24,18 @@ class H(BaseHTTPRequestHandler):
|
|
|
24
24
|
def send(code, obj):
|
|
25
25
|
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
26
26
|
self.wfile.write(json.dumps(obj).encode())
|
|
27
|
+
if self.path.endswith('/pretty'):
|
|
28
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
29
|
+
return self.wfile.write(json.dumps({'success': True, 'data': {'id': 'pretty', 'data': {'text': 'line1\nline2'}, 'rev': 3, 'updatedAt': 't'}}, indent=2).encode())
|
|
30
|
+
if '/field' in self.path:
|
|
31
|
+
from urllib.parse import urlparse, parse_qs
|
|
32
|
+
q = parse_qs(urlparse(self.path).query).get('path', [''])[0]
|
|
33
|
+
if q == '/photo':
|
|
34
|
+
return send(200, {'success': True, 'data': {'field': q, 'type': 'string', 'chars': 38, 'value': 'data:image/png;base64,aGVsbG8gd29ybGQ='}})
|
|
35
|
+
if q == '/n':
|
|
36
|
+
return send(200, {'success': True, 'data': {'field': q, 'type': 'number', 'value': 1}})
|
|
37
|
+
if self.path.endswith('/trunc'):
|
|
38
|
+
return send(200, {'success': True, 'data': {'id': 'trunc', 'data': {'photo': 'xx'}, 'rev': 1, 'updatedAt': 't', 'truncated': [{'field': '/photo', 'chars': 70000}], 'truncatedHint': 'cut'}})
|
|
27
39
|
if self.path.endswith('/missing'):
|
|
28
40
|
return send(404, {'success': False, 'error': 'not_found', 'message': 'Document not found.', 'hint': 'No such app, document or version for this account.'})
|
|
29
41
|
if self.path.endswith('/collaborators') and method == 'GET':
|
|
@@ -89,6 +101,15 @@ check "list: path" "$(jq -r .path "$STUB_LOG")" "/api/apps/$A/data/items?limit=5
|
|
|
89
101
|
OUT=$(run --app $A --get items milk)
|
|
90
102
|
check "get" "$OUT" '{"success":true,"id":"milk","data":{"name":"milk","done":false},"rev":2,"updatedAt":"t"}'
|
|
91
103
|
|
|
104
|
+
# A multi-line (pretty-printed) response body must survive whole (#1122).
|
|
105
|
+
OUT=$(run --app $A --get items pretty)
|
|
106
|
+
check "get: multi-line body kept whole" "$OUT" '{"success":true,"id":"pretty","data":{"text":"line1\nline2"},"rev":3,"updatedAt":"t"}'
|
|
107
|
+
# The missing-session warning goes to stderr and must not break the JSON (#1122).
|
|
108
|
+
OUT=$(env "${ENVS[@]}" CREWLY_SESSION_NAME= bash "$EXEC" --app $A --get items pretty 2>/dev/null)
|
|
109
|
+
check "get: stderr warning does not pollute stdout" "$(printf '%s' "$OUT" | jq -r .id)" "pretty"
|
|
110
|
+
OUT=$(env "${ENVS[@]}" CREWLY_SESSION_NAME= bash "$EXEC" --app $A --get items missing 2>/dev/null; true)
|
|
111
|
+
check "error with warning: reason still parsed" "$(printf '%s' "$OUT" | jq -r .reason)" "not_found"
|
|
112
|
+
|
|
92
113
|
OUT=$(run --app $A --get items missing; true)
|
|
93
114
|
check "get missing → not_found" "$(printf '%s' "$OUT" | jq -c '{success, status, reason}')" '{"success":false,"status":404,"reason":"not_found"}'
|
|
94
115
|
|
|
@@ -195,5 +216,26 @@ check "upload: 2xx without a url fails, not success with nulls" "$(printf '%s' "
|
|
|
195
216
|
OUT=$(run_err --app $A --upload || true)
|
|
196
217
|
check "upload needs a path" "$(printf '%s' "$OUT" | grep -c 'requires a file path')" "1"
|
|
197
218
|
|
|
219
|
+
# --get / --list carry the truncation flag through (#1145).
|
|
220
|
+
OUT=$(run --app $A --get items trunc)
|
|
221
|
+
check "get: truncation flag passed through" "$(printf '%s' "$OUT" | jq -c '.truncated')" '[{"field":"/photo","chars":70000}]'
|
|
222
|
+
|
|
223
|
+
# --get-field --out writes the full value to a file and prints no content (#1145).
|
|
224
|
+
OUT=$(run --app $A --get-field items milk --field /photo --out "$WORK/proj/photo.txt")
|
|
225
|
+
check "get-field: path" "$(jq -r .path "$STUB_LOG")" "/api/apps/$A/data/items/milk/field?path=%2Fphoto"
|
|
226
|
+
check "get-field: file holds the raw value" "$(cat "$WORK/proj/photo.txt")" 'data:image/png;base64,aGVsbG8gd29ybGQ='
|
|
227
|
+
check "get-field: prints a receipt, not the value" "$(printf '%s' "$OUT" | jq -c '{success, bytes, chars}')" '{"success":true,"bytes":38,"chars":38}'
|
|
228
|
+
run --app $A --get-field items milk --field /photo --out "$WORK/proj/photo.bin" --decode >/dev/null
|
|
229
|
+
check "get-field --decode: data URL decoded to bytes" "$(cat "$WORK/proj/photo.bin")" 'hello world'
|
|
230
|
+
OUT=$(run_err --app $A --get-field items milk --field /photo --out "$WORK/outside-photo.txt" || true)
|
|
231
|
+
check "get-field: --out outside the project refused" "$(printf '%s' "$OUT" | grep -c 'inside your project')" "1"
|
|
232
|
+
ln -s "$WORK/outside.json" "$WORK/proj/out-link.txt"
|
|
233
|
+
OUT=$(run_err --app $A --get-field items milk --field /photo --out "$WORK/proj/out-link.txt" || true)
|
|
234
|
+
check "get-field: symlink --out refused" "$(printf '%s' "$OUT" | grep -c 'symbolic link')" "1"
|
|
235
|
+
OUT=$(run_err --app $A --get-field items milk --field /n --out "$WORK/proj/n.txt" || true)
|
|
236
|
+
check "get-field: non-string field refused" "$(printf '%s' "$OUT" | grep -c 'not a string')" "1"
|
|
237
|
+
OUT=$(run_err --app $A --get-field items milk --out "$WORK/proj/x.txt" || true)
|
|
238
|
+
check "get-field: --field required" "$(printf '%s' "$OUT" | grep -c -- '--field is required')" "1"
|
|
239
|
+
|
|
198
240
|
echo "app-data: $PASS passed, $FAIL failed"
|
|
199
241
|
[ "$FAIL" -eq 0 ]
|
|
@@ -60,9 +60,11 @@ On success the skill prints **one JSON line** that always names the `workItemId`
|
|
|
60
60
|
Resolution order:
|
|
61
61
|
|
|
62
62
|
1. `workItemId`, if you pass it — always wins.
|
|
63
|
-
2. Otherwise, a lookup of the WorkItem currently `running` against your
|
|
63
|
+
2. Otherwise, ONLY when you named no task (no `workItemId`, no `taskId`): a lookup of the WorkItem currently `running` against your
|
|
64
64
|
`sessionName` (`GET /api/task-pool/items?status=running&target=<session>`).
|
|
65
|
-
3. If
|
|
65
|
+
3. If you passed `taskId` without `workItemId`, the skill
|
|
66
|
+
refuses (non-zero exit): it never substitutes your running item for an id you named.
|
|
67
|
+
4. If nothing resolves, the skill exits non-zero with an error naming the
|
|
66
68
|
session it searched. It never reports success without completing something.
|
|
67
69
|
|
|
68
70
|
Pass `workItemId` whenever you know it — the lookup in step 2 depends on the
|
|
@@ -124,6 +124,13 @@ fi
|
|
|
124
124
|
#
|
|
125
125
|
# WORK_ITEM_ID was read from the input up top alongside the other params,
|
|
126
126
|
# so the identifier is known before any of the legacy path handling runs.
|
|
127
|
+
# A `taskId` names a specific task: never swap it for whatever happens to be
|
|
128
|
+
# running (#1134 - a complete-task naming one item closed the agent's active,
|
|
129
|
+
# unfinished one). The legacy `absoluteTaskPath` is only logging context and
|
|
130
|
+
# keeps its documented pool lookup.
|
|
131
|
+
if [ -z "$WORK_ITEM_ID" ] && [ -n "$TASK_ID" ]; then
|
|
132
|
+
error_exit "Nothing was completed: you passed taskId '${TASK_ID}' but no 'workItemId', and this skill does not guess which WorkItem you meant. Pass 'workItemId' (find it with GET /api/task-pool/items?target=${SESSION_NAME}) and call again."
|
|
133
|
+
fi
|
|
127
134
|
if [ -z "$WORK_ITEM_ID" ]; then
|
|
128
135
|
POOL_RESP=$(api_call_full GET "/task-pool/items?status=running&target=${SESSION_NAME}" 2>/dev/null || echo '{}')
|
|
129
136
|
WORK_ITEM_ID=$(echo "$POOL_RESP" | jq -r '.workItems[0].id // .data[0].id // empty' 2>/dev/null || true)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for complete-task never swapping a named id for the running item (#1134) — run with: bash explicit-id.test.sh
|
|
3
|
+
set -u
|
|
4
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
EXEC="$HERE/execute.sh"
|
|
6
|
+
PASS=0; FAIL=0
|
|
7
|
+
check() {
|
|
8
|
+
if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; echo " want: $3"; echo " got: $2"; fi
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
PORT=18854
|
|
12
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
13
|
+
: > "$STUB_LOG"
|
|
14
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
15
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
16
|
+
import json, os
|
|
17
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
18
|
+
class H(BaseHTTPRequestHandler):
|
|
19
|
+
def reply(self, code, obj):
|
|
20
|
+
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
21
|
+
self.wfile.write(json.dumps(obj).encode())
|
|
22
|
+
def note(self):
|
|
23
|
+
open(LOG, 'a').write(self.command + ' ' + self.path + '\n')
|
|
24
|
+
def do_GET(self):
|
|
25
|
+
self.note()
|
|
26
|
+
return self.reply(200, {'workItems': [{'id': 'wi-running'}]})
|
|
27
|
+
def do_POST(self):
|
|
28
|
+
n = int(self.headers.get('content-length', '0')); self.rfile.read(n)
|
|
29
|
+
self.note()
|
|
30
|
+
return self.reply(200, {'success': True})
|
|
31
|
+
def log_message(self, *a, **k): pass
|
|
32
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
33
|
+
PY
|
|
34
|
+
STUB_PID=$!
|
|
35
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
36
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
37
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
38
|
+
|
|
39
|
+
run() { CREWLY_SESSION_NAME=dev-1 CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$1" 2>&1; echo "exit=$?"; }
|
|
40
|
+
|
|
41
|
+
: > "$STUB_LOG"
|
|
42
|
+
OUT=$(run '{"taskId":"wi-other","sessionName":"dev-1","summary":"done"}')
|
|
43
|
+
check "taskId without workItemId: refused" "$(printf '%s' "$OUT" | grep -c 'exit=1')" "1"
|
|
44
|
+
check "taskId without workItemId: names the problem" "$(printf '%s' "$OUT" | grep -c "does not guess")" "1"
|
|
45
|
+
check "taskId without workItemId: nothing sent" "$(grep -vc heartbeat "$STUB_LOG")" "0"
|
|
46
|
+
|
|
47
|
+
: > "$STUB_LOG"
|
|
48
|
+
run '{"workItemId":"wi-given","sessionName":"dev-1","summary":"done"}' >/dev/null
|
|
49
|
+
check "explicit workItemId: completes exactly that one" "$(grep -v heartbeat "$STUB_LOG")" "POST /api/task-pool/complete/wi-given"
|
|
50
|
+
|
|
51
|
+
: > "$STUB_LOG"
|
|
52
|
+
run '{"sessionName":"dev-1","summary":"done"}' >/dev/null
|
|
53
|
+
check "no id at all: running-item lookup still works" "$(tail -1 "$STUB_LOG")" "POST /api/task-pool/complete/wi-running"
|
|
54
|
+
|
|
55
|
+
echo "complete-task: $PASS passed, $FAIL failed"
|
|
56
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Docs Comment
|
|
3
|
-
description: Handle feedback left as comments in a Google Doc — list comments and replies
|
|
4
|
-
version: 1.
|
|
3
|
+
description: Handle feedback left as comments in a Google Doc — list comments and replies; reply, resolve and add are queued for the owner's OK and posted as a batch only after they reply 「发 C<n>」 (via the Google Workspace grant held by Crewly Cloud).
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
category: productivity
|
|
6
6
|
skillType: claude-skill
|
|
7
7
|
assignableRoles:
|
|
@@ -41,6 +41,9 @@ bash execute.sh reply --doc 1AbC… --comment AAAB… --text "Updated the intr
|
|
|
41
41
|
bash execute.sh resolve --doc 1AbC… --comment AAAB… --text "Done in v2."
|
|
42
42
|
bash execute.sh resolve --doc 1AbC… --comment AAAB…
|
|
43
43
|
bash execute.sh add --doc 1AbC… --text "Is this number from Q2?" --quote "grew 40%"
|
|
44
|
+
bash execute.sh pending # what is queued and waiting on the owner
|
|
45
|
+
bash execute.sh send --batch C3 # post a batch AFTER the owner said 「发 C3」
|
|
46
|
+
bash execute.sh discard --batch C3
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
`--doc` takes a document id or its docs.google.com URL. `--account` picks a
|
|
@@ -56,8 +59,45 @@ connected Google account (see below).
|
|
|
56
59
|
with a question if it is unclear. Do not resolve a comment you did not
|
|
57
60
|
address.
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
## Posting needs the owner's OK (one OK per batch)
|
|
63
|
+
|
|
64
|
+
Comments and replies on a Google Doc are visible to every collaborator under
|
|
65
|
+
the owner's name, so `reply`, `resolve` and `add` are outbound actions like
|
|
66
|
+
`gmail-send`: **they do not post.** Each call queues the comment and returns
|
|
67
|
+
a batch code:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{"success":true,"held":true,"posted":false,"batch":"C3","queued":38,"message":"Queued as C3 …"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All your queued comments share one batch, so work through the whole document
|
|
74
|
+
first, then ask the owner ONCE, in plain words, with what will go out:
|
|
75
|
+
|
|
76
|
+
> "I have 38 replies ready on the Q3 plan doc (mostly "done, see para 2",
|
|
77
|
+
> 4 questions). Reply 「发 C3」 to post them all."
|
|
78
|
+
|
|
79
|
+
The owner answers from their phone with one message, `发 C3` (or `send C3`).
|
|
80
|
+
Only that genuine owner message counts — not a suggestion, a pre-filled line,
|
|
81
|
+
or your own paraphrase. When the `[SLACK…]`/`[CHAT:…]` message arrives, run:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bash execute.sh send --batch C3
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Rules:
|
|
88
|
+
|
|
89
|
+
- The OK must come AFTER your last queued comment. If you queue one more
|
|
90
|
+
after asking, the earlier OK no longer covers the batch: ask again.
|
|
91
|
+
- `send` posts in order and stops on the first failure; whatever is not
|
|
92
|
+
posted stays queued, so running `send` again never double-posts.
|
|
93
|
+
- A refusal (`needs_owner_confirmation`) means the owner has not said it
|
|
94
|
+
yet. Do not look for another way to post.
|
|
95
|
+
- If the owner wants changes, `discard --batch C3` and queue fresh ones.
|
|
96
|
+
- Queued comments live in this Crewly process; if it restarts before the OK,
|
|
97
|
+
queue them again.
|
|
98
|
+
- `list` is read-only and always open.
|
|
99
|
+
|
|
100
|
+
Tell the owner what you posted once `send` succeeds.
|
|
61
101
|
|
|
62
102
|
## Output
|
|
63
103
|
|
|
@@ -71,6 +111,11 @@ Resolved comments are left out unless you pass `--include-resolved`.
|
|
|
71
111
|
`truncated: true` means the document has more than 500 comments and only the
|
|
72
112
|
first 500 were read.
|
|
73
113
|
|
|
114
|
+
`reply` / `resolve` / `add` queue the comment (see above) until `send --batch`
|
|
115
|
+
succeeds. A successful `send` returns `{"success":true,"batch":"C3","posted":38,"remaining":0,"results":[…]}`;
|
|
116
|
+
on a mid-batch failure `success` is false with `error: "partial"` and the number still queued.
|
|
117
|
+
When the batch posts, each result has the shape below.
|
|
118
|
+
|
|
74
119
|
`reply` / `resolve`:
|
|
75
120
|
|
|
76
121
|
```json
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
# bash execute.sh reply --doc <id|url> --comment <id> --text "…"
|
|
14
14
|
# bash execute.sh resolve --doc <id|url> --comment <id> [--text "…"]
|
|
15
15
|
# bash execute.sh add --doc <id|url> --text "…" [--quote "exact text"]
|
|
16
|
+
# bash execute.sh pending (queued comments awaiting the owner)
|
|
17
|
+
# bash execute.sh send --batch C3 (post a batch the owner approved)
|
|
18
|
+
# bash execute.sh discard --batch C3
|
|
16
19
|
# bash execute.sh '{"command":"list","doc":"<id>"}'
|
|
17
20
|
# =============================================================================
|
|
18
21
|
set -euo pipefail
|
|
@@ -26,6 +29,9 @@ Usage:
|
|
|
26
29
|
bash execute.sh reply --doc <id|url> --comment <id> --text "…"
|
|
27
30
|
bash execute.sh resolve --doc <id|url> --comment <id> [--text "…"]
|
|
28
31
|
bash execute.sh add --doc <id|url> --text "…" [--quote "exact text in the doc"]
|
|
32
|
+
bash execute.sh pending
|
|
33
|
+
bash execute.sh send --batch C3
|
|
34
|
+
bash execute.sh discard --batch C3
|
|
29
35
|
bash execute.sh '{"command":"reply","doc":"<id>","comment":"<id>","text":"…"}'
|
|
30
36
|
|
|
31
37
|
Options:
|
|
@@ -35,6 +41,10 @@ Options:
|
|
|
35
41
|
--quote Exact document text the new comment is about (add only;
|
|
36
42
|
Google Docs may still show it as an unanchored comment)
|
|
37
43
|
--include-resolved list: include resolved comments too
|
|
44
|
+
--batch send / discard: the batch code a reply / resolve / add returned (e.g. C3)
|
|
45
|
+
|
|
46
|
+
reply, resolve and add do NOT post: they queue the comment for the owner and
|
|
47
|
+
return a batch code. After the owner answers 「发 <code>」, run send --batch.
|
|
38
48
|
--account Which connected Google account to act as (default: your primary)
|
|
39
49
|
--help | -h Show this help
|
|
40
50
|
EOF_USAGE
|
|
@@ -66,7 +76,7 @@ elif [[ $# -gt 0 && ${1:0:1} != '-' ]]; then
|
|
|
66
76
|
COMMAND="$1"
|
|
67
77
|
shift
|
|
68
78
|
fi
|
|
69
|
-
DOC=""; COMMENT=""; TEXT=""; TEXT_SET=0; QUOTE=""; INCLUDE_RESOLVED=0; ACCOUNT=""
|
|
79
|
+
DOC=""; COMMENT=""; TEXT=""; TEXT_SET=0; QUOTE=""; INCLUDE_RESOLVED=0; ACCOUNT=""; BATCH=""
|
|
70
80
|
while [[ $# -gt 0 ]]; do
|
|
71
81
|
case "$1" in
|
|
72
82
|
--doc|--id) [ $# -ge 2 ] || error_exit "$1 requires a value"; DOC="$2"; shift 2 ;;
|
|
@@ -75,6 +85,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
75
85
|
--quote) [ $# -ge 2 ] || error_exit "--quote requires a value"; QUOTE="$2"; shift 2 ;;
|
|
76
86
|
--include-resolved) INCLUDE_RESOLVED=1; shift ;;
|
|
77
87
|
--account) [ $# -ge 2 ] || error_exit "--account requires a value"; ACCOUNT="$2"; shift 2 ;;
|
|
88
|
+
--batch) [ $# -ge 2 ] || error_exit "--batch requires a value"; BATCH="$2"; shift 2 ;;
|
|
78
89
|
--help|-h) print_usage; exit 0 ;;
|
|
79
90
|
*) error_exit "Unknown option: $1" ;;
|
|
80
91
|
esac
|
|
@@ -88,13 +99,35 @@ if [ -n "$INPUT_JSON" ]; then
|
|
|
88
99
|
[ -z "$QUOTE" ] && QUOTE=$(printf '%s' "$INPUT" | jq -r '.quote // empty')
|
|
89
100
|
[ "$INCLUDE_RESOLVED" -eq 0 ] && [ "$(printf '%s' "$INPUT" | jq -r '.includeResolved // false')" = "true" ] && INCLUDE_RESOLVED=1
|
|
90
101
|
[ -z "$ACCOUNT" ] && ACCOUNT=$(printf '%s' "$INPUT" | jq -r '.account // empty')
|
|
102
|
+
[ -z "$BATCH" ] && BATCH=$(printf '%s' "$INPUT" | jq -r '.batch // empty')
|
|
91
103
|
fi
|
|
92
104
|
# Route the call at one connected Google account; unset means the default.
|
|
93
105
|
[ -n "$ACCOUNT" ] && export CREWLY_GOOGLE_ACCOUNT="$ACCOUNT"
|
|
94
106
|
# Accept a full docs.google.com URL too.
|
|
95
107
|
DOC=$(printf '%s' "$DOC" | sed -E 's#.*/document/d/([^/?]+).*#\1#')
|
|
96
108
|
|
|
97
|
-
[ -n "$COMMAND" ] || error_exit "a command is required: list, reply, resolve or
|
|
109
|
+
[ -n "$COMMAND" ] || error_exit "a command is required: list, reply, resolve, add, pending, send or discard (see --help)"
|
|
110
|
+
BATCHES="/google/doc-comment-batches"
|
|
111
|
+
# Comment writes are outbound: an agent's are queued, not posted (HTTP 202).
|
|
112
|
+
# Print the queue answer instead of a post result.
|
|
113
|
+
held_or() {
|
|
114
|
+
if printf '%s' "$1" | jq -e '.data.held == true' >/dev/null 2>&1; then
|
|
115
|
+
printf '%s' "$1" | jq -c '{success: true, held: true, posted: false, batch: .data.batch, queued: .data.queued, message: .data.message}'
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
case "$COMMAND" in
|
|
121
|
+
pending)
|
|
122
|
+
RESPONSE=$(call GET "$BATCHES") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
123
|
+
printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), batches: [.data.batches[] | {batch: .code, agent: .agentSession, queued: (.items | length), items: [.items[] | {action, docId, commentId, text, quote}]}]}'
|
|
124
|
+
exit 0 ;;
|
|
125
|
+
send|discard)
|
|
126
|
+
require_param "batch (--batch)" "$BATCH"
|
|
127
|
+
RESPONSE=$(call POST "${BATCHES}/$(uri "$BATCH")/${COMMAND}" '{}') || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
128
|
+
printf '%s' "$RESPONSE" | jq -c '{success: (.success // false)} + .data'
|
|
129
|
+
exit 0 ;;
|
|
130
|
+
esac
|
|
98
131
|
require_param "doc (--doc)" "$DOC"
|
|
99
132
|
BASE="/google/docs/$(uri "$DOC")/comments"
|
|
100
133
|
|
|
@@ -111,18 +144,21 @@ case "$COMMAND" in
|
|
|
111
144
|
require_param "text (--text)" "$TEXT"
|
|
112
145
|
BODY=$(jq -cn --arg text "$TEXT" '{text: $text}')
|
|
113
146
|
RESPONSE=$(call POST "${BASE}/$(uri "$COMMENT")/replies" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
147
|
+
held_or "$RESPONSE" && exit 0
|
|
114
148
|
printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: "reply", docId: .data.docId, commentId: .data.commentId, replyId: .data.id, content: .data.content}'
|
|
115
149
|
;;
|
|
116
150
|
resolve)
|
|
117
151
|
require_param "comment (--comment)" "$COMMENT"
|
|
118
152
|
BODY=$(jq -cn --arg text "$TEXT" 'if $text != "" then {text: $text} else {} end')
|
|
119
153
|
RESPONSE=$(call POST "${BASE}/$(uri "$COMMENT")/resolve" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
154
|
+
held_or "$RESPONSE" && exit 0
|
|
120
155
|
printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: "resolve", docId: .data.docId, commentId: .data.commentId, replyId: .data.id, content: (.data.content // ""), resolved: true}'
|
|
121
156
|
;;
|
|
122
157
|
add)
|
|
123
158
|
require_param "text (--text)" "$TEXT"
|
|
124
159
|
BODY=$(jq -cn --arg text "$TEXT" --arg quote "$QUOTE" '{text: $text} + (if $quote != "" then {quote: $quote} else {} end)')
|
|
125
160
|
RESPONSE=$(call POST "${BASE}" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
161
|
+
held_or "$RESPONSE" && exit 0
|
|
126
162
|
printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), action: "add", docId: .data.docId, commentId: .data.id, content: .data.content} + (if .data.quote then {quote: .data.quote} else {} end)'
|
|
127
163
|
;;
|
|
128
164
|
*)
|
|
@@ -30,6 +30,14 @@ class H(BaseHTTPRequestHandler):
|
|
|
30
30
|
return self._send(403, {'success': False, 'error': 'reauth_required',
|
|
31
31
|
'message': 'Replying to or adding comments on this document needs Google Drive edit access, which this Google account has not granted yet.',
|
|
32
32
|
'hint': 'Ask the owner to reconnect Google Drive: run the google-connect skill with --product drive.'})
|
|
33
|
+
if '/docs/held/' in self.path:
|
|
34
|
+
return self._send(202, {'success': True, 'data': {'held': True, 'batch': 'C3', 'queued': 2, 'message': 'Queued as C3'}})
|
|
35
|
+
if self.path.startswith('/api/google/doc-comment-batches'):
|
|
36
|
+
if method == 'GET':
|
|
37
|
+
return self._send(200, {'success': True, 'data': {'batches': [{'code': 'C3', 'agentSession': 'ella', 'items': [{'action': 'reply', 'docId': 'd1', 'commentId': 'c1', 'text': 'Fixed'}]}]}})
|
|
38
|
+
if '/C9/' in self.path:
|
|
39
|
+
return self._send(403, {'success': False, 'error': 'needs_owner_confirmation', 'message': 'The owner has not confirmed batch C9'})
|
|
40
|
+
return self._send(200, {'success': True, 'data': {'batch': 'C3', 'posted': 2, 'remaining': 0}})
|
|
33
41
|
if method == 'GET':
|
|
34
42
|
return self._send(200, {'success': True, 'data': {'docId': 'd1', 'comments': [COMMENT], 'truncated': False}})
|
|
35
43
|
if self.path.endswith('/replies'):
|
|
@@ -93,6 +101,26 @@ check "reauth: exit code" "$CODE" "1"
|
|
|
93
101
|
check "reauth: reason" "$(printf '%s' "$OUT" | jq -r '.reason')" "reauth_required"
|
|
94
102
|
check "reauth: hint names the one-tap path" "$(printf '%s' "$OUT" | jq -r '.hint' | grep -c 'google-connect skill with --product drive')" "1"
|
|
95
103
|
|
|
104
|
+
# a held comment is reported as queued, never as posted
|
|
105
|
+
for ACT in "reply --doc held --comment c1 --text x" "resolve --doc held --comment c1" "add --doc held --text x"; do
|
|
106
|
+
OUT=$(run $ACT)
|
|
107
|
+
check "held (${ACT%% *}): output" "$OUT" '{"success":true,"held":true,"posted":false,"batch":"C3","queued":2,"message":"Queued as C3"}'
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
# pending / send / discard
|
|
111
|
+
OUT=$(run pending)
|
|
112
|
+
check "pending: request" "$(req)" 'GET /api/google/doc-comment-batches null'
|
|
113
|
+
check "pending: output" "$OUT" '{"success":true,"batches":[{"batch":"C3","agent":"ella","queued":1,"items":[{"action":"reply","docId":"d1","commentId":"c1","text":"Fixed","quote":null}]}]}'
|
|
114
|
+
OUT=$(run send --batch C3)
|
|
115
|
+
check "send: request" "$(req)" 'POST /api/google/doc-comment-batches/C3/send {}'
|
|
116
|
+
check "send: output" "$OUT" '{"success":true,"batch":"C3","posted":2,"remaining":0}'
|
|
117
|
+
run discard --batch C3 >/dev/null
|
|
118
|
+
check "discard: request" "$(req)" 'POST /api/google/doc-comment-batches/C3/discard {}'
|
|
119
|
+
OUT=$(run send --batch C9); CODE=$?
|
|
120
|
+
check "send refused: exit code" "$CODE" "1"
|
|
121
|
+
check "send refused: reason" "$(printf '%s' "$OUT" | jq -r '.reason')" "needs_owner_confirmation"
|
|
122
|
+
check "send needs --batch" "$(run_err send | grep -c 'batch')" "1"
|
|
123
|
+
|
|
96
124
|
# argument validation (no request sent)
|
|
97
125
|
check "no command" "$(run_err --doc d1 | grep -c 'command is required')" "1"
|
|
98
126
|
check "unknown command" "$(run_err delete --doc d1 | grep -c 'Unknown command')" "1"
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
# Returns:
|
|
20
20
|
# On claim: { success: true, claimed: true, workItem: {...}, claim: {...} }
|
|
21
21
|
# No match: { success: true, claimed: false, available: 0, message: "..." }
|
|
22
|
+
# Pending: { success: true, claimed: false, claimPending: true, message: "..." } — the claim timed
|
|
23
|
+
# out and may still land; do not retry, check get-my-tasks later
|
|
22
24
|
# Error: { error: "..." } on stderr, exit 1
|
|
23
25
|
# =============================================================================
|
|
24
26
|
set -euo pipefail
|
|
@@ -168,8 +170,18 @@ else
|
|
|
168
170
|
}')
|
|
169
171
|
fi
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
# The claim is a write the server may apply long after this call gives up
|
|
174
|
+
# (a slow pool-file rewrite queues behind other writes, crewly#1108). Bound
|
|
175
|
+
# the wait and report "pending" instead of failing: a failure invites a blind
|
|
176
|
+
# retry, which queues yet another write.
|
|
177
|
+
CLAIM_RESPONSE=$(CREWLY_API_MAX_TIME="${POLL_TASKS_CLAIM_MAX_TIME:-90}" api_call POST "/task-pool/claim" "$CLAIM_BODY" 2>&1) || {
|
|
172
178
|
CLAIM_ERROR="$CLAIM_RESPONSE"
|
|
179
|
+
# Timed out (curl 28), no HTTP answer (status 0 / 000) or a gateway timeout:
|
|
180
|
+
# the claim may still land. Do not retry; look at get-my-tasks later.
|
|
181
|
+
if printf '%s' "$CLAIM_ERROR" | grep -Eq 'exit code 28|"status":(0|000|408|502|503|504)[,}]'; then
|
|
182
|
+
jq -n '{success: true, claimed: false, claimPending: true, message: "Claim pending: the server has not answered yet and may still apply it. Do NOT retry the claim; check get-my-tasks in a minute to see whether the work item is yours."}'
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
173
185
|
# 404 = no available items (race condition — someone else claimed it)
|
|
174
186
|
if printf '%s' "$CLAIM_ERROR" | grep -q '"status":404'; then
|
|
175
187
|
jq -n '{success: true, claimed: false, available: 0, message: "No work items available (claimed by another agent)"}'
|
|
@@ -297,6 +297,43 @@ assert_json_field "fifo body has no workItemId" "$FIFO_BODY" ".workItemId" "null
|
|
|
297
297
|
assert_json_field "fifo body types[0]" "$FIFO_BODY" ".filters.types[0]" "delegate"
|
|
298
298
|
assert_json_field "fifo body types[1]" "$FIFO_BODY" ".filters.types[1]" "project_task"
|
|
299
299
|
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# Test 9: a claim that times out is reported as pending, not as a failure
|
|
302
|
+
# (crewly#1108). Runs the real execute.sh against a stub curl: GETs answer
|
|
303
|
+
# with one queued item, the claim POST "times out" (curl exit 28).
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
echo "Test 9: claim timeout is reported as claimPending"
|
|
306
|
+
|
|
307
|
+
STUB_DIR=$(mktemp -d)
|
|
308
|
+
cat > "$STUB_DIR/curl" <<'STUB'
|
|
309
|
+
#!/bin/bash
|
|
310
|
+
method=GET
|
|
311
|
+
for a in "$@"; do
|
|
312
|
+
if [ "$prev" = "-X" ]; then method="$a"; fi
|
|
313
|
+
prev="$a"
|
|
314
|
+
done
|
|
315
|
+
if [ "$method" = "POST" ]; then
|
|
316
|
+
[ "$STUB_MODE" = "http504" ] && { printf '{"error":"gateway"}\n504'; exit 0; }
|
|
317
|
+
[ "$STUB_MODE" = "http500" ] && { printf '{"error":"boom"}\n500'; exit 0; }
|
|
318
|
+
exit 28
|
|
319
|
+
fi
|
|
320
|
+
printf '{"success":true,"count":1,"data":[{"id":"wi-1","title":"t","description":"d"}]}\n200'
|
|
321
|
+
STUB
|
|
322
|
+
chmod +x "$STUB_DIR/curl"
|
|
323
|
+
|
|
324
|
+
OUTPUT=$(PATH="$STUB_DIR:$PATH" STUB_MODE=timeout CREWLY_SESSION_NAME=agent-1 bash "$SCRIPT_DIR/execute.sh" '{"sessionName":"agent-1"}' </dev/null 2>/dev/null) && EXIT_CODE=$? || EXIT_CODE=$?
|
|
325
|
+
assert_eq "timeout exits 0" "0" "$EXIT_CODE"
|
|
326
|
+
assert_json_field "timeout: claimPending" "$OUTPUT" ".claimPending" "true"
|
|
327
|
+
assert_json_field "timeout: claimed false" "$OUTPUT" ".claimed" "false"
|
|
328
|
+
assert_contains "timeout: tells the agent not to retry" "Do NOT retry" "$OUTPUT"
|
|
329
|
+
|
|
330
|
+
OUTPUT=$(PATH="$STUB_DIR:$PATH" STUB_MODE=http504 CREWLY_SESSION_NAME=agent-1 bash "$SCRIPT_DIR/execute.sh" '{"sessionName":"agent-1"}' </dev/null 2>/dev/null) && EXIT_CODE=$? || EXIT_CODE=$?
|
|
331
|
+
assert_json_field "504: claimPending" "$OUTPUT" ".claimPending" "true"
|
|
332
|
+
|
|
333
|
+
OUTPUT=$(PATH="$STUB_DIR:$PATH" STUB_MODE=http500 CREWLY_SESSION_NAME=agent-1 bash "$SCRIPT_DIR/execute.sh" '{"sessionName":"agent-1"}' </dev/null 2>&1) && EXIT_CODE=$? || EXIT_CODE=$?
|
|
334
|
+
assert_eq "a real 500 is still an error" "1" "$EXIT_CODE"
|
|
335
|
+
rm -rf "$STUB_DIR"
|
|
336
|
+
|
|
300
337
|
# ---------------------------------------------------------------------------
|
|
301
338
|
# Summary
|
|
302
339
|
# ---------------------------------------------------------------------------
|