crewly 1.20.104 → 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.
Files changed (54) hide show
  1. package/config/skills/agent/core/todo-add/SKILL.md +64 -0
  2. package/config/skills/agent/core/todo-add/execute.sh +84 -0
  3. package/config/skills/agent/core/todo-add/execute.test.sh +76 -0
  4. package/config/skills/agent/core/todo-lists/SKILL.md +58 -0
  5. package/config/skills/agent/core/todo-lists/execute.sh +70 -0
  6. package/config/skills/agent/core/todo-lists/execute.test.sh +77 -0
  7. package/config/skills/agent/core/todo-tasks/SKILL.md +59 -0
  8. package/config/skills/agent/core/todo-tasks/execute.sh +78 -0
  9. package/config/skills/agent/core/todo-tasks/execute.test.sh +74 -0
  10. package/config/skills/agent/core/todo-update/SKILL.md +63 -0
  11. package/config/skills/agent/core/todo-update/execute.sh +115 -0
  12. package/config/skills/agent/core/todo-update/execute.test.sh +82 -0
  13. package/dist/backend/backend/src/constants.d.ts +65 -0
  14. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  15. package/dist/backend/backend/src/constants.js +65 -0
  16. package/dist/backend/backend/src/constants.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.d.ts +51 -0
  18. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.d.ts.map +1 -0
  19. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js +223 -0
  20. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js.map +1 -0
  21. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.d.ts +25 -0
  22. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.d.ts.map +1 -0
  23. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.js +42 -0
  24. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.routes.js.map +1 -0
  25. package/dist/backend/backend/src/index.d.ts.map +1 -1
  26. package/dist/backend/backend/src/index.js +4 -9
  27. package/dist/backend/backend/src/index.js.map +1 -1
  28. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  29. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  30. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  31. package/dist/backend/backend/src/services/agent/restore-filter.d.ts +37 -0
  32. package/dist/backend/backend/src/services/agent/restore-filter.d.ts.map +1 -0
  33. package/dist/backend/backend/src/services/agent/restore-filter.js +48 -0
  34. package/dist/backend/backend/src/services/agent/restore-filter.js.map +1 -0
  35. package/dist/backend/backend/src/services/connector/connector-access.service.d.ts +2 -2
  36. package/dist/backend/backend/src/services/connector/connector-access.service.d.ts.map +1 -1
  37. package/dist/backend/backend/src/services/connector/connector-access.service.js +2 -2
  38. package/dist/backend/backend/src/services/connector/connector-access.service.js.map +1 -1
  39. package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.d.ts +250 -0
  40. package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.d.ts.map +1 -0
  41. package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.js +367 -0
  42. package/dist/backend/backend/src/services/microsoft/microsoft-todo.service.js.map +1 -0
  43. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.d.ts +116 -0
  44. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.d.ts.map +1 -0
  45. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js +220 -0
  46. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js.map +1 -0
  47. package/dist/backend/build-info.json +2 -2
  48. package/dist/cli/backend/src/constants.d.ts +65 -0
  49. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  50. package/dist/cli/backend/src/constants.js +65 -0
  51. package/dist/cli/backend/src/constants.js.map +1 -1
  52. package/frontend/dist/assets/{index-1fd45a2a.js → index-488339c1.js} +103 -103
  53. package/frontend/dist/index.html +1 -1
  54. 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 ]