crewly 1.4.60 → 1.4.62

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 (107) hide show
  1. package/config/skills/agent/content-calendar/execute.sh +2 -2
  2. package/config/skills/agent/marketing/brand-onboarding/SKILL.md +76 -0
  3. package/config/skills/agent/marketing/brand-onboarding/execute.sh +312 -0
  4. package/config/skills/agent/marketing/submit-for-approval/SKILL.md +73 -0
  5. package/config/skills/agent/marketing/submit-for-approval/execute.sh +138 -0
  6. package/config/skills/agent/marketing/weekly-content-planning/SKILL.md +52 -0
  7. package/config/skills/agent/marketing/weekly-content-planning/execute.sh +202 -0
  8. package/config/skills/agent/marketing/weekly-content-planning/execute.test.sh +151 -0
  9. package/config/skills/agent/marketing/weekly-marketing-report/SKILL.md +51 -0
  10. package/config/skills/agent/marketing/weekly-marketing-report/execute.sh +190 -0
  11. package/config/skills/agent/marketing/weekly-marketing-report/execute.test.sh +241 -0
  12. package/config/skills/orchestrator/send-to-remote/SKILL.md +51 -0
  13. package/config/skills/orchestrator/send-to-remote/execute.sh +114 -0
  14. package/config/templates/marketing-team/README.md +42 -0
  15. package/config/templates/marketing-team/goals.md +21 -0
  16. package/config/templates/marketing-team/knowledge/docs/brand-voice-guide.md +61 -0
  17. package/config/templates/marketing-team/knowledge/docs/content-best-practices.md +64 -0
  18. package/config/templates/marketing-team/knowledge/index.json +24 -0
  19. package/config/templates/marketing-team/learned-patterns.json +5 -0
  20. package/config/templates/marketing-team/norms/brand-consistency.md +40 -0
  21. package/config/templates/marketing-team/norms/content-quality-checklist.md +45 -0
  22. package/config/templates/marketing-team/quality-gates.yaml +47 -0
  23. package/config/templates/marketing-team/roles/analyst.md +63 -0
  24. package/config/templates/marketing-team/roles/strategist.md +26 -0
  25. package/config/templates/marketing-team/roles/writer.md +58 -0
  26. package/config/templates/marketing-team/template.json +90 -0
  27. package/config/templates/marketing-team/workflows/weekly-content-cycle.yaml +48 -0
  28. package/dist/backend/backend/src/constants.d.ts +9 -0
  29. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  30. package/dist/backend/backend/src/constants.js +9 -0
  31. package/dist/backend/backend/src/constants.js.map +1 -1
  32. package/dist/backend/backend/src/controllers/cross-machine/cross-machine.controller.d.ts +16 -0
  33. package/dist/backend/backend/src/controllers/cross-machine/cross-machine.controller.d.ts.map +1 -0
  34. package/dist/backend/backend/src/controllers/cross-machine/cross-machine.controller.js +140 -0
  35. package/dist/backend/backend/src/controllers/cross-machine/cross-machine.controller.js.map +1 -0
  36. package/dist/backend/backend/src/controllers/cross-machine/index.d.ts +7 -0
  37. package/dist/backend/backend/src/controllers/cross-machine/index.d.ts.map +1 -0
  38. package/dist/backend/backend/src/controllers/cross-machine/index.js +7 -0
  39. package/dist/backend/backend/src/controllers/cross-machine/index.js.map +1 -0
  40. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  41. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  42. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  43. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/agent/agent-registration.service.js +46 -6
  45. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  46. package/dist/backend/backend/src/services/ai/prompt-builder.service.d.ts +13 -0
  47. package/dist/backend/backend/src/services/ai/prompt-builder.service.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/ai/prompt-builder.service.js +50 -5
  49. package/dist/backend/backend/src/services/ai/prompt-builder.service.js.map +1 -1
  50. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.d.ts +8 -0
  51. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.d.ts.map +1 -1
  52. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.js +52 -3
  53. package/dist/backend/backend/src/services/cloud/device-auto-discovery.service.js.map +1 -1
  54. package/dist/backend/backend/src/services/cloud/relay-client.service.d.ts +5 -1
  55. package/dist/backend/backend/src/services/cloud/relay-client.service.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/cloud/relay-client.service.js +14 -2
  57. package/dist/backend/backend/src/services/cloud/relay-client.service.js.map +1 -1
  58. package/dist/backend/backend/src/services/index.d.ts +2 -0
  59. package/dist/backend/backend/src/services/index.d.ts.map +1 -1
  60. package/dist/backend/backend/src/services/index.js +2 -0
  61. package/dist/backend/backend/src/services/index.js.map +1 -1
  62. package/dist/backend/backend/src/services/onboarding/brand-onboarding.service.d.ts +155 -0
  63. package/dist/backend/backend/src/services/onboarding/brand-onboarding.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/onboarding/brand-onboarding.service.js +469 -0
  65. package/dist/backend/backend/src/services/onboarding/brand-onboarding.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/onboarding/brand-onboarding.types.d.ts +107 -0
  67. package/dist/backend/backend/src/services/onboarding/brand-onboarding.types.d.ts.map +1 -0
  68. package/dist/backend/backend/src/services/onboarding/brand-onboarding.types.js +104 -0
  69. package/dist/backend/backend/src/services/onboarding/brand-onboarding.types.js.map +1 -0
  70. package/dist/backend/backend/src/services/onboarding/content-approval.service.d.ts +124 -0
  71. package/dist/backend/backend/src/services/onboarding/content-approval.service.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/onboarding/content-approval.service.js +256 -0
  73. package/dist/backend/backend/src/services/onboarding/content-approval.service.js.map +1 -0
  74. package/dist/backend/backend/src/services/onboarding/content-approval.types.d.ts +80 -0
  75. package/dist/backend/backend/src/services/onboarding/content-approval.types.d.ts.map +1 -0
  76. package/dist/backend/backend/src/services/onboarding/content-approval.types.js +16 -0
  77. package/dist/backend/backend/src/services/onboarding/content-approval.types.js.map +1 -0
  78. package/dist/backend/backend/src/services/onboarding/index.d.ts +12 -0
  79. package/dist/backend/backend/src/services/onboarding/index.d.ts.map +1 -0
  80. package/dist/backend/backend/src/services/onboarding/index.js +10 -0
  81. package/dist/backend/backend/src/services/onboarding/index.js.map +1 -0
  82. package/dist/backend/backend/src/services/slack/cross-machine-message.service.d.ts +147 -0
  83. package/dist/backend/backend/src/services/slack/cross-machine-message.service.d.ts.map +1 -0
  84. package/dist/backend/backend/src/services/slack/cross-machine-message.service.js +306 -0
  85. package/dist/backend/backend/src/services/slack/cross-machine-message.service.js.map +1 -0
  86. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +7 -0
  87. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  88. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +76 -0
  89. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  90. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/slack/slack.service.js +8 -2
  92. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  93. package/dist/backend/backend/src/types/cross-machine.types.d.ts +103 -0
  94. package/dist/backend/backend/src/types/cross-machine.types.d.ts.map +1 -0
  95. package/dist/backend/backend/src/types/cross-machine.types.js +47 -0
  96. package/dist/backend/backend/src/types/cross-machine.types.js.map +1 -0
  97. package/dist/cli/backend/src/constants.d.ts +9 -0
  98. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  99. package/dist/cli/backend/src/constants.js +9 -0
  100. package/dist/cli/backend/src/constants.js.map +1 -1
  101. package/dist/cli/cli/src/commands/cloud.d.ts +94 -0
  102. package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -0
  103. package/dist/cli/cli/src/commands/cloud.js +323 -0
  104. package/dist/cli/cli/src/commands/cloud.js.map +1 -0
  105. package/dist/cli/cli/src/index.js +17 -0
  106. package/dist/cli/cli/src/index.js.map +1 -1
  107. package/package.json +1 -1
@@ -0,0 +1,202 @@
1
+ #!/bin/bash
2
+ # Weekly Content Planning — generates a weekly content calendar with 5-7 posts
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
6
+
7
+ INPUT="${1:-}"
8
+ [ -z "$INPUT" ] && error_exit "Usage: execute.sh '{\"businessName\":\"...\",\"industry\":\"...\",\"platforms\":[\"x\",\"linkedin\"],\"weekStartDate\":\"YYYY-MM-DD\"}'"
9
+
10
+ # Parse parameters
11
+ BUSINESS_NAME=$(echo "$INPUT" | jq -r '.businessName // empty')
12
+ INDUSTRY=$(echo "$INPUT" | jq -r '.industry // empty')
13
+ PLATFORMS=$(echo "$INPUT" | jq -r '.platforms // empty')
14
+ CONTENT_MIX=$(echo "$INPUT" | jq -r '.contentMix // "{\"educational\":40,\"engagement\":30,\"promotional\":20,\"community\":10}"')
15
+ WEEK_START=$(echo "$INPUT" | jq -r '.weekStartDate // empty')
16
+ PROJECT_PATH=$(echo "$INPUT" | jq -r '.projectPath // empty')
17
+ POST_COUNT=$(echo "$INPUT" | jq -r '.postCount // "5"')
18
+
19
+ require_param "businessName" "$BUSINESS_NAME"
20
+ require_param "industry" "$INDUSTRY"
21
+ require_param "platforms" "$PLATFORMS"
22
+ require_param "weekStartDate" "$WEEK_START"
23
+
24
+ # Validate platforms is a JSON array
25
+ if ! echo "$PLATFORMS" | jq -e 'type == "array"' >/dev/null 2>&1; then
26
+ error_exit "platforms must be a JSON array, e.g. [\"x\",\"linkedin\"]"
27
+ fi
28
+
29
+ PLATFORM_COUNT=$(echo "$PLATFORMS" | jq 'length')
30
+ if [ "$PLATFORM_COUNT" -eq 0 ]; then
31
+ error_exit "platforms array must not be empty"
32
+ fi
33
+
34
+ # Validate content mix
35
+ if ! echo "$CONTENT_MIX" | jq empty 2>/dev/null; then
36
+ CONTENT_MIX='{"educational":40,"engagement":30,"promotional":20,"community":10}'
37
+ fi
38
+
39
+ # Path to content-calendar skill
40
+ CALENDAR_SKILL="${SCRIPT_DIR}/../../content-calendar/execute.sh"
41
+ if [ ! -f "$CALENDAR_SKILL" ]; then
42
+ error_exit "content-calendar skill not found at ${CALENDAR_SKILL}"
43
+ fi
44
+
45
+ # Helper: get topic by category
46
+ get_topic() {
47
+ local cat="$1"
48
+ case "$cat" in
49
+ educational) echo "Industry insight: ${INDUSTRY} trends and best practices" ;;
50
+ engagement) echo "Community question: What matters most to our ${INDUSTRY} audience" ;;
51
+ promotional) echo "Product spotlight: How ${BUSINESS_NAME} solves ${INDUSTRY} challenges" ;;
52
+ community) echo "Behind the scenes: A day at ${BUSINESS_NAME}" ;;
53
+ *) echo "Content for ${BUSINESS_NAME}" ;;
54
+ esac
55
+ }
56
+
57
+ # Compute content mix distribution
58
+ EDU_PCT=$(echo "$CONTENT_MIX" | jq -r '.educational // 40')
59
+ ENG_PCT=$(echo "$CONTENT_MIX" | jq -r '.engagement // 30')
60
+ PROMO_PCT=$(echo "$CONTENT_MIX" | jq -r '.promotional // 20')
61
+ COMM_PCT=$(echo "$CONTENT_MIX" | jq -r '.community // 10')
62
+
63
+ # Calculate post count per category
64
+ TOTAL_PCT=$((EDU_PCT + ENG_PCT + PROMO_PCT + COMM_PCT))
65
+ if [ "$TOTAL_PCT" -eq 0 ]; then
66
+ TOTAL_PCT=100
67
+ fi
68
+
69
+ EDU_COUNT=$(( (POST_COUNT * EDU_PCT + TOTAL_PCT - 1) / TOTAL_PCT ))
70
+ ENG_COUNT=$(( (POST_COUNT * ENG_PCT + TOTAL_PCT - 1) / TOTAL_PCT ))
71
+ PROMO_COUNT=$(( (POST_COUNT * PROMO_PCT + TOTAL_PCT - 1) / TOTAL_PCT ))
72
+ COMM_COUNT=$(( POST_COUNT - EDU_COUNT - ENG_COUNT - PROMO_COUNT ))
73
+ [ "$COMM_COUNT" -lt 0 ] && COMM_COUNT=0
74
+
75
+ # Build ordered category list as space-separated string
76
+ CATEGORIES=""
77
+ i=0; while [ "$i" -lt "$EDU_COUNT" ]; do CATEGORIES="$CATEGORIES educational"; i=$((i+1)); done
78
+ i=0; while [ "$i" -lt "$ENG_COUNT" ]; do CATEGORIES="$CATEGORIES engagement"; i=$((i+1)); done
79
+ i=0; while [ "$i" -lt "$PROMO_COUNT" ]; do CATEGORIES="$CATEGORIES promotional"; i=$((i+1)); done
80
+ i=0; while [ "$i" -lt "$COMM_COUNT" ]; do CATEGORIES="$CATEGORIES community"; i=$((i+1)); done
81
+
82
+ # Trim whitespace and convert to array-like processing
83
+ CATEGORIES=$(echo "$CATEGORIES" | xargs)
84
+
85
+ # Trim to POST_COUNT
86
+ CAT_ARRAY=""
87
+ COUNT=0
88
+ for CAT in $CATEGORIES; do
89
+ if [ "$COUNT" -ge "$POST_COUNT" ]; then break; fi
90
+ CAT_ARRAY="$CAT_ARRAY $CAT"
91
+ COUNT=$((COUNT + 1))
92
+ done
93
+ CAT_ARRAY=$(echo "$CAT_ARRAY" | xargs)
94
+ TOTAL_ENTRIES=$COUNT
95
+
96
+ DAY_NAMES="Mon Tue Wed Thu Fri Sat Sun"
97
+
98
+ # Generate entries
99
+ ENTRIES="[]"
100
+ MARKDOWN_TABLE="| Day | Platform | Type | Category | Topic | Status |\n|-----|----------|------|----------|-------|--------|\n"
101
+
102
+ INDEX=0
103
+ for CATEGORY in $CAT_ARRAY; do
104
+ # Cycle through platforms
105
+ PLATFORM_INDEX=$((INDEX % PLATFORM_COUNT))
106
+ PLATFORM_RAW=$(echo "$PLATFORMS" | jq -r ".[$PLATFORM_INDEX]")
107
+
108
+ # Normalize platform display names to calendar skill identifiers
109
+ case "$(echo "$PLATFORM_RAW" | tr '[:upper:]' '[:lower:]')" in
110
+ "x (twitter)"|"x"|"twitter") PLATFORM="x" ;;
111
+ "linkedin") PLATFORM="linkedin" ;;
112
+ "instagram") PLATFORM="instagram" ;;
113
+ "facebook") PLATFORM="facebook" ;;
114
+ "youtube") PLATFORM="youtube" ;;
115
+ "xiaohongshu"|"小红书") PLATFORM="xiaohongshu" ;;
116
+ "substack") PLATFORM="substack" ;;
117
+ "reddit") PLATFORM="reddit" ;;
118
+ "github") PLATFORM="github" ;;
119
+ *) PLATFORM=$(echo "$PLATFORM_RAW" | tr '[:upper:]' '[:lower:]') ;;
120
+ esac
121
+
122
+ # Calculate the date offset (spread across the week)
123
+ DAY_OFFSET=$((INDEX * 7 / TOTAL_ENTRIES))
124
+
125
+ # Calculate scheduled date
126
+ if date -v+${DAY_OFFSET}d +%Y-%m-%d 2>/dev/null >/dev/null; then
127
+ # macOS: -v flag must come before -f
128
+ SCHED_DATE=$(date -j -v+${DAY_OFFSET}d -f "%Y-%m-%d" "$WEEK_START" +%Y-%m-%d 2>/dev/null || echo "$WEEK_START")
129
+ else
130
+ # GNU date
131
+ SCHED_DATE=$(date -d "$WEEK_START + $DAY_OFFSET days" +%Y-%m-%d 2>/dev/null || echo "$WEEK_START")
132
+ fi
133
+
134
+ # Set content type based on platform
135
+ case "$PLATFORM" in
136
+ x) CONTENT_TYPE="post" ;;
137
+ linkedin) CONTENT_TYPE="post" ;;
138
+ instagram) CONTENT_TYPE="image-text" ;;
139
+ youtube) CONTENT_TYPE="video" ;;
140
+ substack) CONTENT_TYPE="newsletter" ;;
141
+ *) CONTENT_TYPE="post" ;;
142
+ esac
143
+
144
+ TOPIC=$(get_topic "$CATEGORY")
145
+ TITLE="${BUSINESS_NAME} — ${CATEGORY} content for ${PLATFORM} (Week of ${WEEK_START})"
146
+
147
+ # Get day name from offset
148
+ DAY_NAME=$(echo "$DAY_NAMES" | cut -d' ' -f$((DAY_OFFSET + 1)))
149
+
150
+ # Add to content calendar via skill
151
+ ADD_INPUT=$(jq -n \
152
+ --arg action "add" \
153
+ --arg title "$TITLE" \
154
+ --arg platform "$PLATFORM" \
155
+ --arg type "$CONTENT_TYPE" \
156
+ --arg scheduledDate "$SCHED_DATE" \
157
+ --arg status "draft" \
158
+ --arg topic "$TOPIC" \
159
+ --arg notes "Category: ${CATEGORY}. Auto-generated by weekly-content-planning skill." \
160
+ --arg projectPath "$PROJECT_PATH" \
161
+ '{action:$action,title:$title,platform:$platform,type:$type,scheduledDate:$scheduledDate,status:$status,topic:$topic,notes:$notes,projectPath:$projectPath}')
162
+
163
+ RESULT=$(bash "$CALENDAR_SKILL" "$ADD_INPUT" 2>/dev/null || echo '{"success":false}')
164
+
165
+ ENTRY_ID=$(echo "$RESULT" | jq -r '.entry.id // "unknown"')
166
+
167
+ # Build entry object
168
+ ENTRY=$(jq -n \
169
+ --arg id "$ENTRY_ID" \
170
+ --arg day "$DAY_NAME" \
171
+ --arg platform "$PLATFORM" \
172
+ --arg type "$CONTENT_TYPE" \
173
+ --arg category "$CATEGORY" \
174
+ --arg topic "$TOPIC" \
175
+ --arg title "$TITLE" \
176
+ --arg scheduledDate "$SCHED_DATE" \
177
+ --arg status "draft" \
178
+ '{id:$id,day:$day,platform:$platform,type:$type,category:$category,topic:$topic,title:$title,scheduledDate:$scheduledDate,status:$status}')
179
+
180
+ ENTRIES=$(echo "$ENTRIES" | jq --argjson e "$ENTRY" '. += [$e]')
181
+ MARKDOWN_TABLE="${MARKDOWN_TABLE}| ${DAY_NAME} | ${PLATFORM} | ${CONTENT_TYPE} | ${CATEGORY} | ${TOPIC} | draft |\n"
182
+
183
+ INDEX=$((INDEX + 1))
184
+ done
185
+
186
+ ENTRY_COUNT=$(echo "$ENTRIES" | jq 'length')
187
+
188
+ # Output result
189
+ jq -n \
190
+ --arg businessName "$BUSINESS_NAME" \
191
+ --arg weekStart "$WEEK_START" \
192
+ --argjson entries "$ENTRIES" \
193
+ --argjson entryCount "$ENTRY_COUNT" \
194
+ --arg calendar "$(echo -e "$MARKDOWN_TABLE")" \
195
+ '{
196
+ "success": true,
197
+ "businessName": $businessName,
198
+ "weekStartDate": $weekStart,
199
+ "entryCount": $entryCount,
200
+ "entries": $entries,
201
+ "calendar": $calendar
202
+ }'
@@ -0,0 +1,151 @@
1
+ #!/bin/bash
2
+ # Tests for weekly-content-planning skill
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXECUTE="${SCRIPT_DIR}/execute.sh"
7
+ TEST_PROJECT=$(mktemp -d)
8
+ PASS=0
9
+ FAIL=0
10
+
11
+ cleanup() {
12
+ rm -rf "$TEST_PROJECT"
13
+ }
14
+ trap cleanup EXIT
15
+
16
+ assert_eq() {
17
+ local test_name="$1" expected="$2" actual="$3"
18
+ if [ "$expected" = "$actual" ]; then
19
+ echo " ✓ ${test_name}"
20
+ PASS=$((PASS + 1))
21
+ else
22
+ echo " ✗ ${test_name}: expected '${expected}', got '${actual}'"
23
+ FAIL=$((FAIL + 1))
24
+ fi
25
+ }
26
+
27
+ assert_true() {
28
+ local test_name="$1" value="$2"
29
+ if [ "$value" = "true" ]; then
30
+ echo " ✓ ${test_name}"
31
+ PASS=$((PASS + 1))
32
+ else
33
+ echo " ✗ ${test_name}: expected true, got '${value}'"
34
+ FAIL=$((FAIL + 1))
35
+ fi
36
+ }
37
+
38
+ assert_gte() {
39
+ local test_name="$1" expected="$2" actual="$3"
40
+ if [ "$actual" -ge "$expected" ] 2>/dev/null; then
41
+ echo " ✓ ${test_name}"
42
+ PASS=$((PASS + 1))
43
+ else
44
+ echo " ✗ ${test_name}: expected >= ${expected}, got '${actual}'"
45
+ FAIL=$((FAIL + 1))
46
+ fi
47
+ }
48
+
49
+ echo "=== Weekly Content Planning Skill Tests ==="
50
+ echo ""
51
+
52
+ # ─────────────────────────────────────────────
53
+ # Test 1: Missing required parameters
54
+ # ─────────────────────────────────────────────
55
+ echo "Test 1: Missing required parameters"
56
+ RESULT=$(bash "$EXECUTE" '{}' 2>&1 || true)
57
+ HAS_ERROR=$(echo "$RESULT" | jq -r '.error // empty' 2>/dev/null || echo "")
58
+ assert_true "exits with error on missing params" "$([ -n "$HAS_ERROR" ] && echo true || echo false)"
59
+
60
+ # ─────────────────────────────────────────────
61
+ # Test 2: Basic generation with 2 platforms
62
+ # ─────────────────────────────────────────────
63
+ echo "Test 2: Basic generation with 2 platforms"
64
+ RESULT=$(bash "$EXECUTE" "{
65
+ \"businessName\": \"TestCorp\",
66
+ \"industry\": \"technology\",
67
+ \"platforms\": [\"x\", \"linkedin\"],
68
+ \"weekStartDate\": \"2026-03-23\",
69
+ \"projectPath\": \"${TEST_PROJECT}\",
70
+ \"postCount\": \"5\"
71
+ }")
72
+
73
+ assert_true "returns success" "$(echo "$RESULT" | jq -r '.success')"
74
+ assert_eq "businessName matches" "TestCorp" "$(echo "$RESULT" | jq -r '.businessName')"
75
+ assert_eq "weekStartDate matches" "2026-03-23" "$(echo "$RESULT" | jq -r '.weekStartDate')"
76
+ assert_eq "generates 5 entries" "5" "$(echo "$RESULT" | jq -r '.entryCount')"
77
+ assert_true "has calendar markdown" "$(echo "$RESULT" | jq -r '.calendar' | grep -q 'Platform' && echo true || echo false)"
78
+
79
+ # Verify entries are written to calendar.json
80
+ CALENDAR_PATH="${TEST_PROJECT}/.crewly/content/calendar.json"
81
+ assert_true "calendar file created" "$([ -f "$CALENDAR_PATH" ] && echo true || echo false)"
82
+ CALENDAR_ENTRIES=$(jq '.entries | length' "$CALENDAR_PATH")
83
+ assert_eq "calendar has 5 entries" "5" "$CALENDAR_ENTRIES"
84
+
85
+ # ─────────────────────────────────────────────
86
+ # Test 3: Single platform
87
+ # ─────────────────────────────────────────────
88
+ echo "Test 3: Single platform"
89
+ TEST_PROJECT_2=$(mktemp -d)
90
+ RESULT=$(bash "$EXECUTE" "{
91
+ \"businessName\": \"Solo\",
92
+ \"industry\": \"marketing\",
93
+ \"platforms\": [\"x\"],
94
+ \"weekStartDate\": \"2026-04-01\",
95
+ \"projectPath\": \"${TEST_PROJECT_2}\",
96
+ \"postCount\": \"3\"
97
+ }")
98
+
99
+ assert_true "returns success" "$(echo "$RESULT" | jq -r '.success')"
100
+ assert_eq "generates 3 entries" "3" "$(echo "$RESULT" | jq -r '.entryCount')"
101
+
102
+ # All entries should be on x platform
103
+ ALL_X=$(echo "$RESULT" | jq '[.entries[] | select(.platform == "x")] | length')
104
+ assert_eq "all entries on x platform" "3" "$ALL_X"
105
+ rm -rf "$TEST_PROJECT_2"
106
+
107
+ # ─────────────────────────────────────────────
108
+ # Test 4: Invalid platforms format
109
+ # ─────────────────────────────────────────────
110
+ echo "Test 4: Invalid platforms format"
111
+ RESULT=$(bash "$EXECUTE" '{
112
+ "businessName": "Test",
113
+ "industry": "tech",
114
+ "platforms": "not-an-array",
115
+ "weekStartDate": "2026-03-23"
116
+ }' 2>&1 || true)
117
+ HAS_ERROR=$(echo "$RESULT" | jq -r '.error // empty' 2>/dev/null || echo "")
118
+ assert_true "errors on non-array platforms" "$([ -n "$HAS_ERROR" ] && echo true || echo false)"
119
+
120
+ # ─────────────────────────────────────────────
121
+ # Test 5: Entries have correct structure
122
+ # ─────────────────────────────────────────────
123
+ echo "Test 5: Entry structure validation"
124
+ TEST_PROJECT_5=$(mktemp -d)
125
+ RESULT=$(bash "$EXECUTE" "{
126
+ \"businessName\": \"StructTest\",
127
+ \"industry\": \"ai\",
128
+ \"platforms\": [\"linkedin\"],
129
+ \"weekStartDate\": \"2026-03-23\",
130
+ \"projectPath\": \"${TEST_PROJECT_5}\",
131
+ \"postCount\": \"2\"
132
+ }")
133
+
134
+ FIRST_ENTRY=$(echo "$RESULT" | jq '.entries[0]')
135
+ assert_true "entry has id" "$(echo "$FIRST_ENTRY" | jq -r 'has("id")' )"
136
+ assert_true "entry has platform" "$(echo "$FIRST_ENTRY" | jq -r 'has("platform")')"
137
+ assert_true "entry has category" "$(echo "$FIRST_ENTRY" | jq -r 'has("category")')"
138
+ assert_true "entry has topic" "$(echo "$FIRST_ENTRY" | jq -r 'has("topic")')"
139
+ assert_true "entry has scheduledDate" "$(echo "$FIRST_ENTRY" | jq -r 'has("scheduledDate")')"
140
+ assert_eq "entry status is draft" "draft" "$(echo "$FIRST_ENTRY" | jq -r '.status')"
141
+ rm -rf "$TEST_PROJECT_5"
142
+
143
+ # ─────────────────────────────────────────────
144
+ # Summary
145
+ # ─────────────────────────────────────────────
146
+ echo ""
147
+ echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
148
+
149
+ if [ "$FAIL" -gt 0 ]; then
150
+ exit 1
151
+ fi
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: weekly-marketing-report
3
+ description: Generates a structured weekly marketing performance report by reading content calendar data and summarizing posts by platform, status, and completion rate.
4
+ version: 1.0.0
5
+ category: marketing
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - analyst
9
+ triggers:
10
+ - weekly report
11
+ - marketing report
12
+ - performance report
13
+ - analytics report
14
+ tags:
15
+ - marketing
16
+ - analytics
17
+ - report
18
+ - weekly
19
+ execution:
20
+ type: script
21
+ script:
22
+ file: execute.sh
23
+ interpreter: bash
24
+ timeoutMs: 15000
25
+ ---
26
+
27
+ # Weekly Marketing Report
28
+
29
+ Generates a structured weekly marketing performance report by reading content calendar data.
30
+
31
+ ## Input Parameters
32
+
33
+ | Parameter | Required | Description |
34
+ |-----------|----------|-------------|
35
+ | `projectPath` | No | Project path to find calendar data (default: `~/.crewly`) |
36
+ | `weekEndDate` | Yes | End date of the reporting week in `YYYY-MM-DD` format |
37
+ | `businessName` | Yes | Name of the business |
38
+ | `calendarPath` | No | Custom path to calendar.json (overrides default) |
39
+
40
+ ## Example
41
+
42
+ ```bash
43
+ bash execute.sh '{"projectPath":"/path/to/project","weekEndDate":"2026-03-29","businessName":"Acme AI"}'
44
+ ```
45
+
46
+ ## Output
47
+
48
+ Returns JSON with:
49
+ - `success`: boolean
50
+ - `report`: structured report object with summary, platformBreakdown, topPerformers, recommendations
51
+ - `markdown`: formatted markdown report string
@@ -0,0 +1,190 @@
1
+ #!/bin/bash
2
+ # Weekly Marketing Report — reads calendar data and generates a structured performance report
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
6
+
7
+ INPUT="${1:-}"
8
+ [ -z "$INPUT" ] && error_exit "Usage: execute.sh '{\"projectPath\":\"/path\",\"weekEndDate\":\"YYYY-MM-DD\",\"businessName\":\"...\"}'"
9
+
10
+ # Parse parameters
11
+ PROJECT_PATH=$(echo "$INPUT" | jq -r '.projectPath // empty')
12
+ WEEK_END=$(echo "$INPUT" | jq -r '.weekEndDate // empty')
13
+ BUSINESS_NAME=$(echo "$INPUT" | jq -r '.businessName // empty')
14
+ CALENDAR_PATH=$(echo "$INPUT" | jq -r '.calendarPath // empty')
15
+
16
+ require_param "weekEndDate" "$WEEK_END"
17
+ require_param "businessName" "$BUSINESS_NAME"
18
+
19
+ # Determine calendar path
20
+ if [ -z "$CALENDAR_PATH" ]; then
21
+ if [ -n "$PROJECT_PATH" ]; then
22
+ CALENDAR_PATH="${PROJECT_PATH}/.crewly/content/calendar.json"
23
+ else
24
+ CALENDAR_PATH="${HOME}/.crewly/content/calendar.json"
25
+ fi
26
+ fi
27
+
28
+ # Check if calendar file exists
29
+ if [ ! -f "$CALENDAR_PATH" ]; then
30
+ # Return empty report if no calendar data
31
+ jq -n \
32
+ --arg businessName "$BUSINESS_NAME" \
33
+ --arg weekEnd "$WEEK_END" \
34
+ '{
35
+ "success": true,
36
+ "report": {
37
+ "businessName": $businessName,
38
+ "weekEndDate": $weekEnd,
39
+ "summary": "No content calendar data found. Start by creating a weekly content plan.",
40
+ "totalPosts": 0,
41
+ "platformBreakdown": {},
42
+ "statusBreakdown": {},
43
+ "completionRate": 0,
44
+ "topPerformers": [],
45
+ "recommendations": ["Set up a content calendar using the weekly-content-planning skill.", "Define target platforms and content mix ratios.", "Establish baseline metrics for future comparison."]
46
+ },
47
+ "markdown": "# Weekly Marketing Report — Week ending '"$WEEK_END"'\n\n## Summary\nNo content calendar data found. Start by creating a weekly content plan.\n\n## Recommendations\n1. Set up a content calendar using the weekly-content-planning skill.\n2. Define target platforms and content mix ratios.\n3. Establish baseline metrics for future comparison."
48
+ }'
49
+ exit 0
50
+ fi
51
+
52
+ # Validate calendar JSON
53
+ if ! jq empty "$CALENDAR_PATH" 2>/dev/null; then
54
+ error_exit "Calendar file is not valid JSON: $CALENDAR_PATH"
55
+ fi
56
+
57
+ # Calculate week start (6 days before week end = 7-day window)
58
+ if date -v-6d +%Y-%m-%d 2>/dev/null >/dev/null; then
59
+ # macOS: -v flag must come before -f
60
+ WEEK_START=$(date -j -v-6d -f "%Y-%m-%d" "$WEEK_END" +%Y-%m-%d 2>/dev/null || echo "$WEEK_END")
61
+ else
62
+ # GNU/Linux date
63
+ WEEK_START=$(date -d "$WEEK_END - 6 days" +%Y-%m-%d 2>/dev/null || echo "$WEEK_END")
64
+ fi
65
+
66
+ # Filter entries for this week
67
+ WEEK_ENTRIES=$(jq --arg from "$WEEK_START" --arg to "$WEEK_END" \
68
+ '[.entries[] | select(.scheduledDate >= $from and .scheduledDate <= $to)]' \
69
+ "$CALENDAR_PATH")
70
+
71
+ TOTAL_POSTS=$(echo "$WEEK_ENTRIES" | jq 'length')
72
+
73
+ # Posts by platform
74
+ BY_PLATFORM=$(echo "$WEEK_ENTRIES" | jq '[.[] | .platform] | group_by(.) | map({(.[0]): length}) | add // {}')
75
+
76
+ # Posts by status
77
+ BY_STATUS=$(echo "$WEEK_ENTRIES" | jq '[.[] | .status] | group_by(.) | map({(.[0]): length}) | add // {}')
78
+
79
+ # Completion rate (published / total)
80
+ PUBLISHED_COUNT=$(echo "$WEEK_ENTRIES" | jq '[.[] | select(.status == "published")] | length')
81
+ if [ "$TOTAL_POSTS" -gt 0 ]; then
82
+ COMPLETION_RATE=$(( (PUBLISHED_COUNT * 100) / TOTAL_POSTS ))
83
+ else
84
+ COMPLETION_RATE=0
85
+ fi
86
+
87
+ # Posts by content type
88
+ BY_TYPE=$(echo "$WEEK_ENTRIES" | jq '[.[] | .type] | group_by(.) | map({(.[0]): length}) | add // {}')
89
+
90
+ # Draft/ready/approved breakdown for pipeline health
91
+ DRAFT_COUNT=$(echo "$WEEK_ENTRIES" | jq '[.[] | select(.status == "draft")] | length')
92
+ READY_COUNT=$(echo "$WEEK_ENTRIES" | jq '[.[] | select(.status == "ready")] | length')
93
+ APPROVED_COUNT=$(echo "$WEEK_ENTRIES" | jq '[.[] | select(.status == "approved")] | length')
94
+
95
+ # Top performers — published entries (most recently published)
96
+ TOP_PERFORMERS=$(echo "$WEEK_ENTRIES" | jq '[.[] | select(.status == "published")] | sort_by(.publishedAt) | reverse | .[:3] | [.[] | {title: .title, platform: .platform, type: .type, scheduledDate: .scheduledDate}]')
97
+
98
+ # Generate recommendations
99
+ RECOMMENDATIONS="[]"
100
+
101
+ if [ "$TOTAL_POSTS" -eq 0 ]; then
102
+ RECOMMENDATIONS=$(echo "$RECOMMENDATIONS" | jq '. += ["No posts scheduled this week. Create a content plan using the weekly-content-planning skill."]')
103
+ elif [ "$TOTAL_POSTS" -lt 5 ]; then
104
+ RECOMMENDATIONS=$(echo "$RECOMMENDATIONS" | jq --argjson count "$TOTAL_POSTS" '. += ["Only \($count) posts scheduled. Target 5-7 posts per week for consistent presence."]')
105
+ fi
106
+
107
+ if [ "$DRAFT_COUNT" -gt 0 ]; then
108
+ RECOMMENDATIONS=$(echo "$RECOMMENDATIONS" | jq --argjson count "$DRAFT_COUNT" '. += ["\($count) posts still in draft. Prioritize moving them to ready status."]')
109
+ fi
110
+
111
+ if [ "$COMPLETION_RATE" -lt 50 ] && [ "$TOTAL_POSTS" -gt 0 ]; then
112
+ RECOMMENDATIONS=$(echo "$RECOMMENDATIONS" | jq --argjson rate "$COMPLETION_RATE" '. += ["Completion rate is \($rate)%. Investigate bottlenecks in the content pipeline."]')
113
+ fi
114
+
115
+ PLATFORM_COUNT=$(echo "$BY_PLATFORM" | jq 'keys | length')
116
+ if [ "$PLATFORM_COUNT" -le 1 ] && [ "$TOTAL_POSTS" -gt 0 ]; then
117
+ RECOMMENDATIONS=$(echo "$RECOMMENDATIONS" | jq '. += ["Content concentrated on single platform. Diversify across 2-3 platforms for wider reach."]')
118
+ fi
119
+
120
+ # If no specific recommendations, add general ones
121
+ REC_COUNT=$(echo "$RECOMMENDATIONS" | jq 'length')
122
+ if [ "$REC_COUNT" -eq 0 ]; then
123
+ RECOMMENDATIONS='["Continue current posting cadence — metrics are on track.", "Experiment with one new content format next week.", "Review engagement data to identify top-performing content themes."]'
124
+ fi
125
+
126
+ # Build markdown report
127
+ MARKDOWN="# Weekly Marketing Report — ${BUSINESS_NAME}\n"
128
+ MARKDOWN="${MARKDOWN}## Week of ${WEEK_START} to ${WEEK_END}\n\n"
129
+ MARKDOWN="${MARKDOWN}## Executive Summary\n"
130
+ MARKDOWN="${MARKDOWN}Total posts scheduled: ${TOTAL_POSTS} | Published: ${PUBLISHED_COUNT} | Completion rate: ${COMPLETION_RATE}%\n\n"
131
+ MARKDOWN="${MARKDOWN}## Platform Breakdown\n"
132
+ MARKDOWN="${MARKDOWN}$(echo "$BY_PLATFORM" | jq -r 'to_entries | .[] | "- **\(.key)**: \(.value) posts"')\n\n"
133
+ MARKDOWN="${MARKDOWN}## Status Breakdown\n"
134
+ MARKDOWN="${MARKDOWN}$(echo "$BY_STATUS" | jq -r 'to_entries | .[] | "- **\(.key)**: \(.value)"')\n\n"
135
+ MARKDOWN="${MARKDOWN}## Pipeline Health\n"
136
+ MARKDOWN="${MARKDOWN}| Stage | Count |\n|-------|-------|\n"
137
+ MARKDOWN="${MARKDOWN}| Draft | ${DRAFT_COUNT} |\n"
138
+ MARKDOWN="${MARKDOWN}| Ready | ${READY_COUNT} |\n"
139
+ MARKDOWN="${MARKDOWN}| Approved | ${APPROVED_COUNT} |\n"
140
+ MARKDOWN="${MARKDOWN}| Published | ${PUBLISHED_COUNT} |\n\n"
141
+
142
+ if [ "$(echo "$TOP_PERFORMERS" | jq 'length')" -gt 0 ]; then
143
+ MARKDOWN="${MARKDOWN}## Top Performers\n"
144
+ MARKDOWN="${MARKDOWN}$(echo "$TOP_PERFORMERS" | jq -r '.[] | "- **\(.title)** — \(.platform) (\(.type))"')\n\n"
145
+ fi
146
+
147
+ MARKDOWN="${MARKDOWN}## Recommendations\n"
148
+ MARKDOWN="${MARKDOWN}$(echo "$RECOMMENDATIONS" | jq -r 'to_entries | .[] | "\(.key + 1). \(.value)"')\n"
149
+
150
+ # Build structured report JSON
151
+ jq -n \
152
+ --arg businessName "$BUSINESS_NAME" \
153
+ --arg weekStart "$WEEK_START" \
154
+ --arg weekEnd "$WEEK_END" \
155
+ --argjson totalPosts "$TOTAL_POSTS" \
156
+ --argjson publishedCount "$PUBLISHED_COUNT" \
157
+ --argjson completionRate "$COMPLETION_RATE" \
158
+ --argjson platformBreakdown "$BY_PLATFORM" \
159
+ --argjson statusBreakdown "$BY_STATUS" \
160
+ --argjson typeBreakdown "$BY_TYPE" \
161
+ --argjson topPerformers "$TOP_PERFORMERS" \
162
+ --argjson recommendations "$RECOMMENDATIONS" \
163
+ --argjson draftCount "$DRAFT_COUNT" \
164
+ --argjson readyCount "$READY_COUNT" \
165
+ --argjson approvedCount "$APPROVED_COUNT" \
166
+ --arg markdown "$(echo -e "$MARKDOWN")" \
167
+ '{
168
+ "success": true,
169
+ "report": {
170
+ "businessName": $businessName,
171
+ "weekStartDate": $weekStart,
172
+ "weekEndDate": $weekEnd,
173
+ "summary": "Total: \($totalPosts) posts | Published: \($publishedCount) | Completion: \($completionRate)%",
174
+ "totalPosts": $totalPosts,
175
+ "publishedCount": $publishedCount,
176
+ "completionRate": $completionRate,
177
+ "platformBreakdown": $platformBreakdown,
178
+ "statusBreakdown": $statusBreakdown,
179
+ "typeBreakdown": $typeBreakdown,
180
+ "pipelineHealth": {
181
+ "draft": $draftCount,
182
+ "ready": $readyCount,
183
+ "approved": $approvedCount,
184
+ "published": $publishedCount
185
+ },
186
+ "topPerformers": $topPerformers,
187
+ "recommendations": $recommendations
188
+ },
189
+ "markdown": $markdown
190
+ }'