crewly 1.20.105 → 1.20.106
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/todo-add/SKILL.md +64 -0
- package/config/skills/agent/core/todo-add/execute.sh +84 -0
- package/config/skills/agent/core/todo-add/execute.test.sh +76 -0
- package/config/skills/agent/core/todo-lists/SKILL.md +58 -0
- package/config/skills/agent/core/todo-lists/execute.sh +70 -0
- package/config/skills/agent/core/todo-lists/execute.test.sh +77 -0
- package/config/skills/agent/core/todo-tasks/SKILL.md +59 -0
- package/config/skills/agent/core/todo-tasks/execute.sh +78 -0
- package/config/skills/agent/core/todo-tasks/execute.test.sh +74 -0
- package/config/skills/agent/core/todo-update/SKILL.md +63 -0
- package/config/skills/agent/core/todo-update/execute.sh +115 -0
- package/config/skills/agent/core/todo-update/execute.test.sh +82 -0
- package/dist/backend/backend/src/constants.d.ts +65 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +65 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.d.ts +51 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js +223 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.d.ts +25 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.js +42 -0
- package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.js.map +1 -0
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +3 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/connector/connector-access.service.d.ts +2 -2
- package/dist/backend/backend/src/services/connector/connector-access.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/connector/connector-access.service.js +2 -2
- package/dist/backend/backend/src/services/connector/connector-access.service.js.map +1 -1
- package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.d.ts +250 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.js +367 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.js.map +1 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-token.service.d.ts +116 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-token.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js +220 -0
- package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +65 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +65 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/frontend/dist/assets/{index-1fd45a2a.js → index-488339c1.js} +103 -103
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: To Do Add
|
|
3
|
+
description: Add a task to one of the owner's Microsoft To Do lists — title, optional due date, note and importance (via the Microsoft grant held by Crewly Cloud).
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- developer
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- sales
|
|
14
|
+
- support
|
|
15
|
+
- generalist
|
|
16
|
+
triggers:
|
|
17
|
+
- add to my to do list
|
|
18
|
+
- add a microsoft to do task
|
|
19
|
+
- remind me in to do
|
|
20
|
+
- create an outlook task
|
|
21
|
+
tags:
|
|
22
|
+
- microsoft
|
|
23
|
+
- todo
|
|
24
|
+
- tasks
|
|
25
|
+
execution:
|
|
26
|
+
type: script
|
|
27
|
+
script:
|
|
28
|
+
file: execute.sh
|
|
29
|
+
interpreter: bash
|
|
30
|
+
timeoutMs: 60000
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# To Do Add
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bash execute.sh --title "Buy milk" # default list ("Tasks")
|
|
37
|
+
bash execute.sh --list Work --title "Send deck to Ann" --due 2026-10-01 --importance high --note "v3 is in Drive"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Flag | Meaning |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `--title` | Task title (required) |
|
|
43
|
+
| `--list` | List name (case-insensitive) or id; default list when omitted |
|
|
44
|
+
| `--due` | Due date `YYYY-MM-DD` |
|
|
45
|
+
| `--note` | Note shown under the task |
|
|
46
|
+
| `--importance` | `low` \| `normal` \| `high` |
|
|
47
|
+
|
|
48
|
+
## Output
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{"success":true,"list":"Work","task":{"id":"AAMk…","title":"Send deck to Ann","status":"notStarted","importance":"high","due":"2026-10-01","note":"v3 is in Drive"}}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A list that does not exist is not created — make it with todo-lists
|
|
55
|
+
`--create` first.
|
|
56
|
+
|
|
57
|
+
## Failures
|
|
58
|
+
|
|
59
|
+
`{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
|
|
60
|
+
when the owner has not connected Microsoft To Do (Connections → Microsoft To Do).
|
|
61
|
+
Other reasons: `not_found` (unknown list / task — the message lists the
|
|
62
|
+
available lists), `forbidden` (the account has no Exchange Online mailbox, or
|
|
63
|
+
the organisation blocks the app), `rate_limited` (with `retryAfter` seconds),
|
|
64
|
+
`validation`, `connector_forbidden` (the owner has not allowed your role).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# todo-add — Add a task to a Microsoft To Do list
|
|
4
|
+
#
|
|
5
|
+
# Backed by POST /api/microsoft-todo/tasks.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash execute.sh --title "Buy milk" # default list
|
|
9
|
+
# bash execute.sh --list Work --title "Send deck to Ann" --due 2026-10-01 --importance high --note "v3 in Drive"
|
|
10
|
+
# bash execute.sh '{"list":"Groceries","title":"Eggs"}'
|
|
11
|
+
# =============================================================================
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
15
|
+
|
|
16
|
+
print_usage() {
|
|
17
|
+
cat <<'EOF_USAGE'
|
|
18
|
+
Usage:
|
|
19
|
+
bash execute.sh --title "Buy milk" # default list
|
|
20
|
+
bash execute.sh --list Work --title "Send deck to Ann" --due 2026-10-01 --importance high --note "v3 in Drive"
|
|
21
|
+
bash execute.sh '{"list":"Groceries","title":"Eggs"}'
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--title, -t Task title (required)
|
|
25
|
+
--list, -l List name (case-insensitive) or id; default: the owner's default list
|
|
26
|
+
--due Due date, YYYY-MM-DD
|
|
27
|
+
--note Note text shown under the task
|
|
28
|
+
--importance low | normal | high
|
|
29
|
+
--help | -h Show this help
|
|
30
|
+
EOF_USAGE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fail_from() {
|
|
34
|
+
printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")} + (if (.details | type) == "object" and .details.retryAfter then {retryAfter: .details.retryAfter} else {} end)' 2>/dev/null \
|
|
35
|
+
|| jq -n --arg r "$1" '{success: false, reason: $r}'
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
40
|
+
|
|
41
|
+
# api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
|
|
42
|
+
# the backend answer is always the last line. On failure print the mapped
|
|
43
|
+
# failure JSON (fail_from) and return 1.
|
|
44
|
+
call() {
|
|
45
|
+
local out
|
|
46
|
+
out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
|
|
47
|
+
printf '%s\n' "$out" | tail -n 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
INPUT_JSON=""
|
|
51
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
52
|
+
INPUT_JSON="$1"
|
|
53
|
+
shift || true
|
|
54
|
+
fi
|
|
55
|
+
TITLE=""; LIST=""; DUE=""; NOTE=""; IMPORTANCE=""
|
|
56
|
+
while [[ $# -gt 0 ]]; do
|
|
57
|
+
case "$1" in
|
|
58
|
+
--title|-t) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
|
|
59
|
+
--list|-l) [ $# -ge 2 ] || error_exit "--list requires a value"; LIST="$2"; shift 2 ;;
|
|
60
|
+
--due) [ $# -ge 2 ] || error_exit "--due requires a value"; DUE="$2"; shift 2 ;;
|
|
61
|
+
--note) [ $# -ge 2 ] || error_exit "--note requires a value"; NOTE="$2"; shift 2 ;;
|
|
62
|
+
--importance) [ $# -ge 2 ] || error_exit "--importance requires a value"; IMPORTANCE="$2"; shift 2 ;;
|
|
63
|
+
--help|-h) print_usage; exit 0 ;;
|
|
64
|
+
*) error_exit "Unknown option: $1" ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
68
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
69
|
+
[ -z "$TITLE" ] && TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
|
|
70
|
+
[ -z "$LIST" ] && LIST=$(printf '%s' "$INPUT" | jq -r '.list // empty')
|
|
71
|
+
[ -z "$DUE" ] && DUE=$(printf '%s' "$INPUT" | jq -r '.due // empty')
|
|
72
|
+
[ -z "$NOTE" ] && NOTE=$(printf '%s' "$INPUT" | jq -r '.note // empty')
|
|
73
|
+
[ -z "$IMPORTANCE" ] && IMPORTANCE=$(printf '%s' "$INPUT" | jq -r '.importance // empty')
|
|
74
|
+
fi
|
|
75
|
+
[ -n "$TITLE" ] || error_exit "--title is required"
|
|
76
|
+
if [ -n "$DUE" ] && ! [[ "$DUE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then error_exit "--due must look like 2026-10-01"; fi
|
|
77
|
+
BODY=$(jq -cn --arg title "$TITLE" --arg list "$LIST" --arg due "$DUE" --arg note "$NOTE" --arg imp "$IMPORTANCE" \
|
|
78
|
+
'{title: $title}
|
|
79
|
+
+ (if $list != "" then {list: $list} else {} end)
|
|
80
|
+
+ (if $due != "" then {due: $due} else {} end)
|
|
81
|
+
+ (if $note != "" then {note: $note} else {} end)
|
|
82
|
+
+ (if $imp != "" then {importance: $imp} else {} end)')
|
|
83
|
+
RESPONSE=$(call POST "/microsoft-todo/tasks" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
84
|
+
printf '%s' "$RESPONSE" | jq -c '{success: true, list: .data.list.name, task: .data.task}'
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for todo-add — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18832
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
LIST = {'id': 'L1', 'name': 'Groceries'}
|
|
21
|
+
class H(BaseHTTPRequestHandler):
|
|
22
|
+
def _reply(self, method, data=None):
|
|
23
|
+
open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
|
|
24
|
+
if 'no-grant' in self.path or 'no-grant' in json.dumps(data or {}):
|
|
25
|
+
self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
26
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
|
|
27
|
+
path = self.path.split('?')[0]
|
|
28
|
+
if path == '/api/microsoft-todo/lists' and method == 'GET':
|
|
29
|
+
out = {'count': 2, 'lists': [{'id': 'L0', 'name': 'Tasks', 'isDefault': True}, LIST]}
|
|
30
|
+
elif path == '/api/microsoft-todo/lists':
|
|
31
|
+
out = {'id': 'L9', 'name': (data or {}).get('name')}
|
|
32
|
+
elif path == '/api/microsoft-todo/tasks' and method == 'GET':
|
|
33
|
+
out = {'list': LIST, 'count': 1, 'tasks': [{'id': 'T1', 'title': 'Milk', 'status': 'notStarted', 'due': '2026-10-01'}]}
|
|
34
|
+
elif path == '/api/microsoft-todo/tasks':
|
|
35
|
+
out = {'list': LIST, 'task': {'id': 'T2', 'title': (data or {}).get('title'), 'status': 'notStarted'}}
|
|
36
|
+
elif method == 'PATCH':
|
|
37
|
+
out = {'list': LIST, 'task': {'id': path.rsplit('/', 1)[1], 'title': 'Milk', 'status': 'completed'}}
|
|
38
|
+
elif method == 'DELETE':
|
|
39
|
+
out = {'list': LIST, 'taskId': path.rsplit('/', 1)[1], 'deleted': True}
|
|
40
|
+
else:
|
|
41
|
+
out = {}
|
|
42
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
43
|
+
self.wfile.write(json.dumps({'success': True, 'data': out}).encode())
|
|
44
|
+
def _body(self):
|
|
45
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
46
|
+
return json.loads(body or '{}')
|
|
47
|
+
def do_GET(self): self._reply('GET')
|
|
48
|
+
def do_DELETE(self): self._reply('DELETE')
|
|
49
|
+
def do_POST(self): self._reply('POST', self._body())
|
|
50
|
+
def do_PATCH(self): self._reply('PATCH', self._body())
|
|
51
|
+
def log_message(self, *a, **k): pass
|
|
52
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
53
|
+
PY
|
|
54
|
+
STUB_PID=$!
|
|
55
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
56
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
57
|
+
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
|
|
58
|
+
|
|
59
|
+
run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
60
|
+
run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
|
|
61
|
+
last() { jq -c "$1" "$STUB_LOG"; }
|
|
62
|
+
|
|
63
|
+
OUT=$(run --list Work --title "Send deck" --due 2026-10-01 --importance high --note "v3 in Drive")
|
|
64
|
+
check "add: output" "$OUT" '{"success":true,"list":"Groceries","task":{"id":"T2","title":"Send deck","status":"notStarted"}}'
|
|
65
|
+
check "add: request" "$(last '[.method,.path,.body]')" '["POST","/api/microsoft-todo/tasks",{"title":"Send deck","list":"Work","due":"2026-10-01","note":"v3 in Drive","importance":"high"}]'
|
|
66
|
+
run --title "Buy milk" >/dev/null
|
|
67
|
+
check "add: default list omits list" "$(last '.body')" '{"title":"Buy milk"}'
|
|
68
|
+
run '{"list":"Groceries","title":"Eggs"}' >/dev/null
|
|
69
|
+
check "json input" "$(last '.body')" '{"title":"Eggs","list":"Groceries"}'
|
|
70
|
+
check "title required" "$(run_err --list Work | jq -r .error)" "--title is required"
|
|
71
|
+
check "bad due" "$(run_err --title x --due tomorrow | jq -r .error)" "--due must look like 2026-10-01"
|
|
72
|
+
OUT=$(run --title no-grant || true)
|
|
73
|
+
check "not connected" "$(printf '%s' "$OUT" | jq -r .reason)" "not_connected"
|
|
74
|
+
|
|
75
|
+
echo "todo-add: $PASS passed, $FAIL failed"
|
|
76
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: To Do Lists
|
|
3
|
+
description: List the owner's Microsoft To Do task lists, or create a new list (via the Microsoft grant held by Crewly Cloud).
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- developer
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- sales
|
|
14
|
+
- support
|
|
15
|
+
- generalist
|
|
16
|
+
triggers:
|
|
17
|
+
- microsoft to do lists
|
|
18
|
+
- which to do lists
|
|
19
|
+
- create a to do list
|
|
20
|
+
- outlook tasks lists
|
|
21
|
+
tags:
|
|
22
|
+
- microsoft
|
|
23
|
+
- todo
|
|
24
|
+
- tasks
|
|
25
|
+
execution:
|
|
26
|
+
type: script
|
|
27
|
+
script:
|
|
28
|
+
file: execute.sh
|
|
29
|
+
interpreter: bash
|
|
30
|
+
timeoutMs: 60000
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# To Do Lists
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bash execute.sh
|
|
37
|
+
bash execute.sh --create "Trip to Lisbon"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Output
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{"count":2,"lists":[{"id":"AAMk…","name":"Tasks","isDefault":true},{"id":"AAMk…","name":"Groceries","isShared":true}]}
|
|
44
|
+
{"success":true,"list":{"id":"AAMk…","name":"Trip to Lisbon"}}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The other todo-* skills take a list **name** (case-insensitive) or id, so
|
|
48
|
+
you rarely need the id. `isDefault` marks the list that answers when no
|
|
49
|
+
list is named.
|
|
50
|
+
|
|
51
|
+
## Failures
|
|
52
|
+
|
|
53
|
+
`{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
|
|
54
|
+
when the owner has not connected Microsoft To Do (Connections → Microsoft To Do).
|
|
55
|
+
Other reasons: `not_found` (unknown list / task — the message lists the
|
|
56
|
+
available lists), `forbidden` (the account has no Exchange Online mailbox, or
|
|
57
|
+
the organisation blocks the app), `rate_limited` (with `retryAfter` seconds),
|
|
58
|
+
`validation`, `connector_forbidden` (the owner has not allowed your role).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# todo-lists — List the owner's Microsoft To Do lists, or create one
|
|
4
|
+
#
|
|
5
|
+
# Backed by GET / POST /api/microsoft-todo/lists.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash execute.sh
|
|
9
|
+
# bash execute.sh --create "Trip to Lisbon"
|
|
10
|
+
# bash execute.sh '{"create":"Trip to Lisbon"}'
|
|
11
|
+
# =============================================================================
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
15
|
+
|
|
16
|
+
print_usage() {
|
|
17
|
+
cat <<'EOF_USAGE'
|
|
18
|
+
Usage:
|
|
19
|
+
bash execute.sh
|
|
20
|
+
bash execute.sh --create "Trip to Lisbon"
|
|
21
|
+
bash execute.sh '{"create":"Trip to Lisbon"}'
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--create Create a list with this name instead of listing
|
|
25
|
+
--help | -h Show this help
|
|
26
|
+
EOF_USAGE
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fail_from() {
|
|
30
|
+
printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")} + (if (.details | type) == "object" and .details.retryAfter then {retryAfter: .details.retryAfter} else {} end)' 2>/dev/null \
|
|
31
|
+
|| jq -n --arg r "$1" '{success: false, reason: $r}'
|
|
32
|
+
exit 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
36
|
+
|
|
37
|
+
# api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
|
|
38
|
+
# the backend answer is always the last line. On failure print the mapped
|
|
39
|
+
# failure JSON (fail_from) and return 1.
|
|
40
|
+
call() {
|
|
41
|
+
local out
|
|
42
|
+
out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
|
|
43
|
+
printf '%s\n' "$out" | tail -n 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
INPUT_JSON=""
|
|
47
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
48
|
+
INPUT_JSON="$1"
|
|
49
|
+
shift || true
|
|
50
|
+
fi
|
|
51
|
+
CREATE=""
|
|
52
|
+
while [[ $# -gt 0 ]]; do
|
|
53
|
+
case "$1" in
|
|
54
|
+
--create) [ $# -ge 2 ] || error_exit "--create requires a value"; CREATE="$2"; shift 2 ;;
|
|
55
|
+
--help|-h) print_usage; exit 0 ;;
|
|
56
|
+
*) error_exit "Unknown option: $1" ;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
60
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
61
|
+
[ -z "$CREATE" ] && CREATE=$(printf '%s' "$INPUT" | jq -r '.create // .name // empty')
|
|
62
|
+
fi
|
|
63
|
+
if [ -n "$CREATE" ]; then
|
|
64
|
+
BODY=$(jq -cn --arg name "$CREATE" '{name: $name}')
|
|
65
|
+
RESPONSE=$(call POST "/microsoft-todo/lists" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
66
|
+
printf '%s' "$RESPONSE" | jq -c '{success: true, list: .data}'
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
RESPONSE=$(call GET "/microsoft-todo/lists") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
70
|
+
printf '%s' "$RESPONSE" | jq -c '{count: .data.count, lists: .data.lists}'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for todo-lists — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18830
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
LIST = {'id': 'L1', 'name': 'Groceries'}
|
|
21
|
+
class H(BaseHTTPRequestHandler):
|
|
22
|
+
def _reply(self, method, data=None):
|
|
23
|
+
open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
|
|
24
|
+
if 'no-grant' in self.path or 'no-grant' in json.dumps(data or {}):
|
|
25
|
+
self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
26
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
|
|
27
|
+
path = self.path.split('?')[0]
|
|
28
|
+
if path == '/api/microsoft-todo/lists' and method == 'GET':
|
|
29
|
+
out = {'count': 2, 'lists': [{'id': 'L0', 'name': 'Tasks', 'isDefault': True}, LIST]}
|
|
30
|
+
elif path == '/api/microsoft-todo/lists':
|
|
31
|
+
out = {'id': 'L9', 'name': (data or {}).get('name')}
|
|
32
|
+
elif path == '/api/microsoft-todo/tasks' and method == 'GET':
|
|
33
|
+
out = {'list': LIST, 'count': 1, 'tasks': [{'id': 'T1', 'title': 'Milk', 'status': 'notStarted', 'due': '2026-10-01'}]}
|
|
34
|
+
elif path == '/api/microsoft-todo/tasks':
|
|
35
|
+
out = {'list': LIST, 'task': {'id': 'T2', 'title': (data or {}).get('title'), 'status': 'notStarted'}}
|
|
36
|
+
elif method == 'PATCH':
|
|
37
|
+
out = {'list': LIST, 'task': {'id': path.rsplit('/', 1)[1], 'title': 'Milk', 'status': 'completed'}}
|
|
38
|
+
elif method == 'DELETE':
|
|
39
|
+
out = {'list': LIST, 'taskId': path.rsplit('/', 1)[1], 'deleted': True}
|
|
40
|
+
else:
|
|
41
|
+
out = {}
|
|
42
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
43
|
+
self.wfile.write(json.dumps({'success': True, 'data': out}).encode())
|
|
44
|
+
def _body(self):
|
|
45
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
46
|
+
return json.loads(body or '{}')
|
|
47
|
+
def do_GET(self): self._reply('GET')
|
|
48
|
+
def do_DELETE(self): self._reply('DELETE')
|
|
49
|
+
def do_POST(self): self._reply('POST', self._body())
|
|
50
|
+
def do_PATCH(self): self._reply('PATCH', self._body())
|
|
51
|
+
def log_message(self, *a, **k): pass
|
|
52
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
53
|
+
PY
|
|
54
|
+
STUB_PID=$!
|
|
55
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
56
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
57
|
+
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
|
|
58
|
+
|
|
59
|
+
run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
60
|
+
run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
|
|
61
|
+
last() { jq -c "$1" "$STUB_LOG"; }
|
|
62
|
+
|
|
63
|
+
OUT=$(run)
|
|
64
|
+
check "list: output" "$OUT" '{"count":2,"lists":[{"id":"L0","name":"Tasks","isDefault":true},{"id":"L1","name":"Groceries"}]}'
|
|
65
|
+
check "list: request" "$(last '[.method,.path]')" '["GET","/api/microsoft-todo/lists"]'
|
|
66
|
+
OUT=$(run --create "Trip to Lisbon")
|
|
67
|
+
check "create: output" "$OUT" '{"success":true,"list":{"id":"L9","name":"Trip to Lisbon"}}'
|
|
68
|
+
check "create: request" "$(last '[.method,.path,.body]')" '["POST","/api/microsoft-todo/lists",{"name":"Trip to Lisbon"}]'
|
|
69
|
+
run '{"create":"Holiday"}' >/dev/null
|
|
70
|
+
check "json create" "$(last '.body.name')" '"Holiday"'
|
|
71
|
+
OUT=$(run --create no-grant || true)
|
|
72
|
+
check "not connected: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "not_connected"
|
|
73
|
+
check "not connected: hint" "$(printf '%s' "$OUT" | jq -r .hint)" "https://cloud/start?token=j"
|
|
74
|
+
check "unknown flag" "$(run_err --bogus | jq -r .error)" "Unknown option: --bogus"
|
|
75
|
+
|
|
76
|
+
echo "todo-lists: $PASS passed, $FAIL failed"
|
|
77
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: To Do Tasks
|
|
3
|
+
description: List the tasks in one of the owner's Microsoft To Do lists — open tasks by default, --all to include completed ones (via the Microsoft grant held by Crewly Cloud). Read-only.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- developer
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- sales
|
|
14
|
+
- support
|
|
15
|
+
- generalist
|
|
16
|
+
triggers:
|
|
17
|
+
- what is on my to do list
|
|
18
|
+
- list microsoft to do tasks
|
|
19
|
+
- open tasks in to do
|
|
20
|
+
- outlook tasks
|
|
21
|
+
tags:
|
|
22
|
+
- microsoft
|
|
23
|
+
- todo
|
|
24
|
+
- tasks
|
|
25
|
+
execution:
|
|
26
|
+
type: script
|
|
27
|
+
script:
|
|
28
|
+
file: execute.sh
|
|
29
|
+
interpreter: bash
|
|
30
|
+
timeoutMs: 60000
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# To Do Tasks
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bash execute.sh # open tasks of the default list ("Tasks")
|
|
37
|
+
bash execute.sh --list Groceries # name (case-insensitive) or id
|
|
38
|
+
bash execute.sh --list Work --all # include completed tasks
|
|
39
|
+
bash execute.sh --list Work --limit 20 # default 50, max 100
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Output
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{"list":{"id":"AAMk…","name":"Work"},"count":2,"tasks":[{"id":"AAMk…","title":"Send deck to Ann","status":"notStarted","importance":"high","due":"2026-10-01","note":"v3 is in Drive"},{"id":"AAMk…","title":"Book venue","status":"completed","completedAt":"2026-09-20"}]}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`importance` appears only when not normal; `note` is plain text, cut at
|
|
49
|
+
200 characters. `hasMore: true` means the list has more tasks than the
|
|
50
|
+
limit. Use the task `id` with todo-update.
|
|
51
|
+
|
|
52
|
+
## Failures
|
|
53
|
+
|
|
54
|
+
`{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
|
|
55
|
+
when the owner has not connected Microsoft To Do (Connections → Microsoft To Do).
|
|
56
|
+
Other reasons: `not_found` (unknown list / task — the message lists the
|
|
57
|
+
available lists), `forbidden` (the account has no Exchange Online mailbox, or
|
|
58
|
+
the organisation blocks the app), `rate_limited` (with `retryAfter` seconds),
|
|
59
|
+
`validation`, `connector_forbidden` (the owner has not allowed your role).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# todo-tasks — List the tasks of one Microsoft To Do list (open ones unless --all)
|
|
4
|
+
#
|
|
5
|
+
# Backed by GET /api/microsoft-todo/tasks.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash execute.sh # open tasks of the default list ("Tasks")
|
|
9
|
+
# bash execute.sh --list Groceries # list name (case-insensitive) or id
|
|
10
|
+
# bash execute.sh --list Work --all # include completed tasks
|
|
11
|
+
# bash execute.sh '{"list":"Work","limit":20}'
|
|
12
|
+
# =============================================================================
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
16
|
+
|
|
17
|
+
print_usage() {
|
|
18
|
+
cat <<'EOF_USAGE'
|
|
19
|
+
Usage:
|
|
20
|
+
bash execute.sh # open tasks of the default list ("Tasks")
|
|
21
|
+
bash execute.sh --list Groceries # list name (case-insensitive) or id
|
|
22
|
+
bash execute.sh --list Work --all # include completed tasks
|
|
23
|
+
bash execute.sh '{"list":"Work","limit":20}'
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--list, -l List name (case-insensitive) or id; default: the owner's default list
|
|
27
|
+
--all Include completed tasks
|
|
28
|
+
--limit, -n Result cap (default 50, max 100)
|
|
29
|
+
--help | -h Show this help
|
|
30
|
+
EOF_USAGE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fail_from() {
|
|
34
|
+
printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")} + (if (.details | type) == "object" and .details.retryAfter then {retryAfter: .details.retryAfter} else {} end)' 2>/dev/null \
|
|
35
|
+
|| jq -n --arg r "$1" '{success: false, reason: $r}'
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
40
|
+
|
|
41
|
+
# api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
|
|
42
|
+
# the backend answer is always the last line. On failure print the mapped
|
|
43
|
+
# failure JSON (fail_from) and return 1.
|
|
44
|
+
call() {
|
|
45
|
+
local out
|
|
46
|
+
out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s\n' "$out" | tail -n 1)"; }
|
|
47
|
+
printf '%s\n' "$out" | tail -n 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
INPUT_JSON=""
|
|
51
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
52
|
+
INPUT_JSON="$1"
|
|
53
|
+
shift || true
|
|
54
|
+
fi
|
|
55
|
+
LIST=""; ALL=""; LIMIT=""
|
|
56
|
+
while [[ $# -gt 0 ]]; do
|
|
57
|
+
case "$1" in
|
|
58
|
+
--list|-l) [ $# -ge 2 ] || error_exit "--list requires a value"; LIST="$2"; shift 2 ;;
|
|
59
|
+
--all) ALL="1"; shift ;;
|
|
60
|
+
--limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
|
|
61
|
+
--help|-h) print_usage; exit 0 ;;
|
|
62
|
+
*) error_exit "Unknown option: $1" ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
66
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
67
|
+
[ -z "$LIST" ] && LIST=$(printf '%s' "$INPUT" | jq -r '.list // empty')
|
|
68
|
+
[ -z "$ALL" ] && ALL=$(printf '%s' "$INPUT" | jq -r 'if .all == true then "1" else empty end')
|
|
69
|
+
[ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
|
|
70
|
+
fi
|
|
71
|
+
QS=""
|
|
72
|
+
[ -n "$LIST" ] && QS="${QS}&list=$(uri "$LIST")"
|
|
73
|
+
[ -n "$ALL" ] && QS="${QS}&all=1"
|
|
74
|
+
[ -n "$LIMIT" ] && QS="${QS}&limit=$(uri "$LIMIT")"
|
|
75
|
+
QS="${QS#&}"
|
|
76
|
+
RESPONSE=$(call GET "/microsoft-todo/tasks${QS:+?$QS}") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
77
|
+
if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
|
|
78
|
+
printf '%s' "$RESPONSE" | jq -c '{list: .data.list, count: .data.count, tasks: .data.tasks} + (if .data.hasMore then {hasMore: true} else {} end)'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for todo-tasks — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18831
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
LIST = {'id': 'L1', 'name': 'Groceries'}
|
|
21
|
+
class H(BaseHTTPRequestHandler):
|
|
22
|
+
def _reply(self, method, data=None):
|
|
23
|
+
open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
|
|
24
|
+
if 'no-grant' in self.path or 'no-grant' in json.dumps(data or {}):
|
|
25
|
+
self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
26
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
|
|
27
|
+
path = self.path.split('?')[0]
|
|
28
|
+
if path == '/api/microsoft-todo/lists' and method == 'GET':
|
|
29
|
+
out = {'count': 2, 'lists': [{'id': 'L0', 'name': 'Tasks', 'isDefault': True}, LIST]}
|
|
30
|
+
elif path == '/api/microsoft-todo/lists':
|
|
31
|
+
out = {'id': 'L9', 'name': (data or {}).get('name')}
|
|
32
|
+
elif path == '/api/microsoft-todo/tasks' and method == 'GET':
|
|
33
|
+
out = {'list': LIST, 'count': 1, 'tasks': [{'id': 'T1', 'title': 'Milk', 'status': 'notStarted', 'due': '2026-10-01'}]}
|
|
34
|
+
elif path == '/api/microsoft-todo/tasks':
|
|
35
|
+
out = {'list': LIST, 'task': {'id': 'T2', 'title': (data or {}).get('title'), 'status': 'notStarted'}}
|
|
36
|
+
elif method == 'PATCH':
|
|
37
|
+
out = {'list': LIST, 'task': {'id': path.rsplit('/', 1)[1], 'title': 'Milk', 'status': 'completed'}}
|
|
38
|
+
elif method == 'DELETE':
|
|
39
|
+
out = {'list': LIST, 'taskId': path.rsplit('/', 1)[1], 'deleted': True}
|
|
40
|
+
else:
|
|
41
|
+
out = {}
|
|
42
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
43
|
+
self.wfile.write(json.dumps({'success': True, 'data': out}).encode())
|
|
44
|
+
def _body(self):
|
|
45
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
46
|
+
return json.loads(body or '{}')
|
|
47
|
+
def do_GET(self): self._reply('GET')
|
|
48
|
+
def do_DELETE(self): self._reply('DELETE')
|
|
49
|
+
def do_POST(self): self._reply('POST', self._body())
|
|
50
|
+
def do_PATCH(self): self._reply('PATCH', self._body())
|
|
51
|
+
def log_message(self, *a, **k): pass
|
|
52
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
53
|
+
PY
|
|
54
|
+
STUB_PID=$!
|
|
55
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
56
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
57
|
+
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
|
|
58
|
+
|
|
59
|
+
run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
60
|
+
run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
|
|
61
|
+
last() { jq -c "$1" "$STUB_LOG"; }
|
|
62
|
+
|
|
63
|
+
OUT=$(run --list "Big Shop" --all --limit 5)
|
|
64
|
+
check "list: output" "$OUT" '{"list":{"id":"L1","name":"Groceries"},"count":1,"tasks":[{"id":"T1","title":"Milk","status":"notStarted","due":"2026-10-01"}]}'
|
|
65
|
+
check "list: path" "$(last '.path')" '"/api/microsoft-todo/tasks?list=Big%20Shop&all=1&limit=5"'
|
|
66
|
+
run >/dev/null
|
|
67
|
+
check "default list: bare path" "$(last '.path')" '"/api/microsoft-todo/tasks"'
|
|
68
|
+
run '{"list":"Work","all":true}' >/dev/null
|
|
69
|
+
check "json input" "$(last '.path')" '"/api/microsoft-todo/tasks?list=Work&all=1"'
|
|
70
|
+
OUT=$(run --list no-grant || true)
|
|
71
|
+
check "not connected" "$(printf '%s' "$OUT" | jq -r .reason)" "not_connected"
|
|
72
|
+
|
|
73
|
+
echo "todo-tasks: $PASS passed, $FAIL failed"
|
|
74
|
+
[ "$FAIL" -eq 0 ]
|