crewly 1.5.14 → 1.5.15

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.
@@ -21,8 +21,10 @@ CREWLY_API_URL="${CREWLY_API_URL:-http://localhost:8787}"
21
21
  # and replaces the script's positional parameters with the file contents.
22
22
  # All downstream parsing (read_json_input, custom arg loops) works unchanged.
23
23
  #
24
- # Usage (by LLM CLI):
25
- # printf '%s' '{"summary":"text with 'quotes'"}' > /tmp/crewly_input.json
24
+ # Usage (by LLM CLI — use heredoc to avoid single-quote EOF issues):
25
+ # cat > /tmp/crewly_input.json << 'CREWLY_EOF'
26
+ # {"summary":"text with 'quotes' and special chars"}
27
+ # CREWLY_EOF
26
28
  # bash execute.sh --file /tmp/crewly_input.json
27
29
  #
28
30
  # This runs at source-time, so every script that sources lib.sh gets it for free.
@@ -156,6 +156,34 @@ echo '{}' > "$TEMP_DIR/empty.json"
156
156
  RESULT=$(bash "$TEMP_DIR/test_read_json.sh" --file "$TEMP_DIR/empty.json")
157
157
  assert_eq "--file with empty JSON" '{}' "$RESULT"
158
158
 
159
+ # ---- Test 14: --file with heredoc-created file (single quotes in JSON) ----
160
+ # This simulates the exact pattern Gemini CLI agents now use:
161
+ # cat > /tmp/file << 'CREWLY_EOF'
162
+ # {"summary":"it's working — don't worry"}
163
+ # CREWLY_EOF
164
+ cat > "$TEMP_DIR/heredoc_single_quotes.json" << 'CREWLY_EOF'
165
+ {"summary":"it's working — don't worry about 'edge cases'"}
166
+ CREWLY_EOF
167
+ RESULT=$(bash "$TEMP_DIR/test_skill.sh" --file "$TEMP_DIR/heredoc_single_quotes.json")
168
+ assert_contains "heredoc with single quotes" "it's working" "$RESULT"
169
+ assert_contains "heredoc with multiple single quotes" "edge cases" "$RESULT"
170
+
171
+ # ---- Test 15: --file with heredoc-created file (backticks and $vars) ----
172
+ cat > "$TEMP_DIR/heredoc_special.json" << 'CREWLY_EOF'
173
+ {"text":"Use `jq` to parse $HOME and $(whoami) safely"}
174
+ CREWLY_EOF
175
+ RESULT=$(bash "$TEMP_DIR/test_skill.sh" --file "$TEMP_DIR/heredoc_special.json")
176
+ assert_contains "heredoc preserves backticks" '`jq`' "$RESULT"
177
+ assert_contains "heredoc preserves \$HOME" '$HOME' "$RESULT"
178
+ assert_contains "heredoc preserves \$()" '$(whoami)' "$RESULT"
179
+
180
+ # ---- Test 16: --file with heredoc-created file (mixed quotes) ----
181
+ cat > "$TEMP_DIR/heredoc_mixed.json" << 'CREWLY_EOF'
182
+ {"text":"He said \"it's fine\" and she said 'OK'"}
183
+ CREWLY_EOF
184
+ RESULT=$(bash "$TEMP_DIR/test_skill.sh" --file "$TEMP_DIR/heredoc_mixed.json")
185
+ assert_contains "heredoc handles mixed quotes" "it's fine" "$RESULT"
186
+
159
187
  echo ""
160
188
  echo "=== Results: $PASS/$TOTAL passed, $FAIL failed ==="
161
189
 
@@ -15,7 +15,7 @@
15
15
  set -euo pipefail
16
16
 
17
17
  # ── Configuration ──────────────────────────────────────────────────────────────
18
- MODEL="${GEMINI_MODEL:-gemini-2.5-flash-preview-05-20}"
18
+ MODEL="${GEMINI_MODEL:-gemini-2.0-flash}"
19
19
  GEMINI_API_BASE="https://generativelanguage.googleapis.com"
20
20
  GEMINI_CONTENT_URL="${GEMINI_API_BASE}/v1beta/models/${MODEL}:generateContent"
21
21
  MAX_IMAGE_SIZE_MB=4
@@ -149,12 +149,12 @@ REQUEST_BODY=$(cat <<ENDJSON
149
149
  ENDJSON
150
150
  )
151
151
 
152
- TMPFILE=$(mktemp /tmp/gemini-compare-XXXXXX.json)
152
+ TMPFILE="/tmp/gemini-compare-$(date +%s).json"
153
153
  echo "$REQUEST_BODY" > "$TMPFILE"
154
- RESPONSE=$(curl -sf -X POST \
154
+ RESPONSE=$(curl -s -X POST \
155
155
  "${GEMINI_CONTENT_URL}?key=${GEMINI_API_KEY}" \
156
156
  -H "Content-Type: application/json" \
157
- -d "@${TMPFILE}" 2>&1)
157
+ -d "@${TMPFILE}")
158
158
  rm -f "$TMPFILE"
159
159
 
160
160
  HTTP_CODE=$?
@@ -0,0 +1,52 @@
1
+ # xhs-article-to-image
2
+
3
+ Convert markdown articles into XiaoHongShu (小红书) style image cards.
4
+
5
+ ## Description
6
+
7
+ Generates multiple PNG images (1080×1440, 3:4 portrait ratio) from markdown article content, styled in the popular XiaoHongShu magazine-layout format: bold titles, clean white backgrounds, structured paragraphs with visual hierarchy.
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ bash execute.sh '{"content":"# Title\n\nArticle body...","outputDir":"/tmp/xhs-output"}'
13
+ ```
14
+
15
+ Or from a file:
16
+ ```bash
17
+ bash execute.sh '{"sourceFile":"/path/to/article.md","outputDir":"/tmp/xhs-output"}'
18
+ ```
19
+
20
+ ## Parameters
21
+
22
+ | Parameter | Required | Default | Description |
23
+ |-----------|----------|---------|-------------|
24
+ | content | Yes* | - | Markdown article content (required if no sourceFile) |
25
+ | sourceFile | Yes* | - | Path to markdown file (required if no content) |
26
+ | outputDir | Yes | - | Directory to write PNG images to |
27
+ | title | No | auto | Override the article title (auto-extracts from first H1) |
28
+ | author | No | "" | Author name shown at bottom of pages |
29
+ | accentColor | No | "#1a1a1a" | Accent color for decorative elements |
30
+ | bgColor | No | "#fafaf8" | Background color |
31
+ | maxCharsPerPage | No | 350 | Approximate characters per page before splitting |
32
+ | coverImage | No | "" | URL or path to image for the cover page |
33
+
34
+ ## Output
35
+
36
+ - Multiple PNG files: `page-01.png`, `page-02.png`, etc.
37
+ - First page is the title/cover page
38
+ - Returns JSON with file paths and metadata
39
+
40
+ ## Dependencies
41
+
42
+ - Node.js 18+
43
+ - Playwright (chromium)
44
+
45
+ ## Style Features
46
+
47
+ - 3:4 portrait ratio (1080×1440px) optimized for XHS
48
+ - Bold sans-serif Chinese/English typography (Noto Sans SC)
49
+ - Clean white/cream background with subtle decorative elements
50
+ - Magazine-style paragraph layout with visual breathing room
51
+ - Page numbering (current/total)
52
+ - Optional author attribution
@@ -0,0 +1,120 @@
1
+ #!/bin/bash
2
+ # Convert markdown articles into XiaoHongShu-style image cards (1080x1440 PNG)
3
+ # Uses Playwright to render HTML+CSS templates and capture screenshots.
4
+ set -euo pipefail
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "${SCRIPT_DIR}/../_common/lib.sh"
7
+
8
+ INPUT=$(read_json_input "${1:-}")
9
+ [ -z "$INPUT" ] && error_exit "Usage: execute.sh '{\"content\":\"# Title\\n\\nBody...\",\"outputDir\":\"/tmp/xhs\"}' or '{\"sourceFile\":\"/path/to/article.md\",\"outputDir\":\"/tmp/xhs\"}'"
10
+
11
+ # Parse parameters
12
+ CONTENT=$(printf '%s' "$INPUT" | jq -r '.content // empty')
13
+ SOURCE_FILE=$(printf '%s' "$INPUT" | jq -r '.sourceFile // empty')
14
+ OUTPUT_DIR=$(printf '%s' "$INPUT" | jq -r '.outputDir // empty')
15
+ TITLE_OVERRIDE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
16
+ AUTHOR=$(printf '%s' "$INPUT" | jq -r '.author // empty')
17
+ ACCENT_COLOR=$(printf '%s' "$INPUT" | jq -r '.accentColor // "#1a1a1a"')
18
+ BG_COLOR=$(printf '%s' "$INPUT" | jq -r '.bgColor // "#fafaf8"')
19
+ MAX_CHARS=$(printf '%s' "$INPUT" | jq -r '.maxCharsPerPage // "350"')
20
+ COVER_IMAGE=$(printf '%s' "$INPUT" | jq -r '.coverImage // empty')
21
+
22
+ # Load content from file if sourceFile is provided
23
+ if [ -n "$SOURCE_FILE" ]; then
24
+ if [ ! -f "$SOURCE_FILE" ]; then
25
+ error_exit "Source file not found: $SOURCE_FILE"
26
+ fi
27
+ CONTENT=$(cat "$SOURCE_FILE")
28
+ fi
29
+
30
+ require_param "content" "$CONTENT"
31
+ require_param "outputDir" "$OUTPUT_DIR"
32
+
33
+ # Create output directory
34
+ mkdir -p "$OUTPUT_DIR"
35
+
36
+ # Extract title from markdown (first # heading)
37
+ if [ -n "$TITLE_OVERRIDE" ]; then
38
+ TITLE="$TITLE_OVERRIDE"
39
+ else
40
+ TITLE=$(echo "$CONTENT" | grep -m1 '^# ' | sed 's/^# //' || echo "Untitled")
41
+ if [ -z "$TITLE" ]; then
42
+ TITLE="Untitled"
43
+ fi
44
+ fi
45
+
46
+ # Remove title line from content body
47
+ BODY=$(echo "$CONTENT" | sed '0,/^# .*$/d')
48
+ if [ -z "$BODY" ]; then
49
+ BODY="$CONTENT"
50
+ fi
51
+
52
+ # ─── Split content into pages ─────────────────────────────────────────────────
53
+ # Split on explicit --- page breaks first, then by character count
54
+
55
+ # Use node to do the splitting and rendering (complex text processing)
56
+ RENDER_INPUT=$(jq -n \
57
+ --arg title "$TITLE" \
58
+ --arg author "$AUTHOR" \
59
+ --arg accentColor "$ACCENT_COLOR" \
60
+ --arg bgColor "$BG_COLOR" \
61
+ --arg outputDir "$OUTPUT_DIR" \
62
+ --arg coverImage "$COVER_IMAGE" \
63
+ --arg body "$BODY" \
64
+ --argjson maxChars "$MAX_CHARS" \
65
+ '{
66
+ title: $title,
67
+ author: $author,
68
+ accentColor: $accentColor,
69
+ bgColor: $bgColor,
70
+ outputDir: $outputDir,
71
+ coverImage: $coverImage,
72
+ maxChars: $maxChars,
73
+ body: $body
74
+ }')
75
+
76
+ # Use Node.js to split content into pages (handles complex markdown parsing)
77
+ PAGES_JSON=$(node -e "
78
+ const input = JSON.parse(process.argv[1]);
79
+ const body = input.body;
80
+ const maxChars = input.maxChars;
81
+
82
+ function splitIntoPages(markdown, maxChars) {
83
+ const explicitSections = markdown.split(/\n---\n/).filter(s => s.trim());
84
+ const pages = [];
85
+ for (const section of explicitSections) {
86
+ if (section.length <= maxChars) {
87
+ pages.push(section.trim());
88
+ continue;
89
+ }
90
+ const blocks = section.split(/\n\n/).filter(b => b.trim());
91
+ let currentPage = '';
92
+ for (const block of blocks) {
93
+ if (currentPage.length > 0 && currentPage.length + block.length > maxChars) {
94
+ pages.push(currentPage.trim());
95
+ currentPage = block;
96
+ } else {
97
+ currentPage += (currentPage ? '\n\n' : '') + block;
98
+ }
99
+ }
100
+ if (currentPage.trim()) pages.push(currentPage.trim());
101
+ }
102
+ return pages.length > 0 ? pages : [markdown.trim()];
103
+ }
104
+
105
+ const pages = splitIntoPages(body, maxChars);
106
+ console.log(JSON.stringify(pages));
107
+ " "$RENDER_INPUT")
108
+
109
+ # Build final render input with pages array
110
+ FINAL_INPUT=$(printf '%s' "$RENDER_INPUT" | jq --argjson pages "$PAGES_JSON" '. + {pages: $pages} | del(.body, .maxChars)')
111
+
112
+ # Run Playwright renderer
113
+ RESULT=$(node "$SCRIPT_DIR/render.mjs" "$FINAL_INPUT" 2>&1)
114
+
115
+ # Check for errors
116
+ if echo "$RESULT" | jq -e '.success == true' > /dev/null 2>&1; then
117
+ echo "$RESULT" | jq --arg title "$TITLE" '. + {title: $title}'
118
+ else
119
+ error_exit "Rendering failed: $RESULT"
120
+ fi
@@ -0,0 +1,601 @@
1
+ /**
2
+ * XHS Article-to-Image Renderer
3
+ *
4
+ * Converts markdown article content into multiple XiaoHongShu-style
5
+ * PNG image cards (1080×1440, 3:4 portrait) using Playwright.
6
+ *
7
+ * Input: JSON via argv[2] with { pages, title, author, accentColor, bgColor, outputDir, coverImage }
8
+ * Output: PNG files written to outputDir, JSON result to stdout.
9
+ */
10
+ import { chromium } from 'playwright';
11
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
12
+ import { join, resolve } from 'path';
13
+
14
+ const WIDTH = 1080;
15
+ const HEIGHT = 1440;
16
+
17
+ const input = JSON.parse(process.argv[2]);
18
+ const {
19
+ pages,
20
+ title = 'Untitled',
21
+ author = '',
22
+ accentColor = '#1a1a1a',
23
+ bgColor = '#fafaf8',
24
+ outputDir,
25
+ coverImage = '',
26
+ } = input;
27
+
28
+ mkdirSync(outputDir, { recursive: true });
29
+
30
+ /**
31
+ * Generate the HTML for the cover (first) page.
32
+ *
33
+ * @param {string} title - Article title
34
+ * @param {string} author - Author name
35
+ * @param {string} accentColor - Accent color hex
36
+ * @param {string} bgColor - Background color hex
37
+ * @param {string} coverImage - Optional cover image URL/path
38
+ * @param {number} totalPages - Total page count
39
+ * @returns {string} Complete HTML string
40
+ */
41
+ function buildCoverHTML(title, author, accentColor, bgColor, coverImage, totalPages) {
42
+ const imageBlock = coverImage
43
+ ? `<div class="cover-image"><img src="${coverImage}" alt="cover" /></div>`
44
+ : '';
45
+
46
+ return `<!DOCTYPE html>
47
+ <html>
48
+ <head>
49
+ <meta charset="utf-8">
50
+ <style>
51
+ ${getBaseStyles(bgColor, accentColor)}
52
+
53
+ .cover-container {
54
+ display: flex;
55
+ flex-direction: column;
56
+ justify-content: center;
57
+ align-items: center;
58
+ height: 100%;
59
+ padding: 100px 80px;
60
+ box-sizing: border-box;
61
+ text-align: center;
62
+ }
63
+
64
+ .cover-decoration-top {
65
+ width: 60px;
66
+ height: 4px;
67
+ background: ${accentColor};
68
+ margin-bottom: 50px;
69
+ border-radius: 2px;
70
+ }
71
+
72
+ .cover-title {
73
+ font-size: 72px;
74
+ font-weight: 900;
75
+ line-height: 1.4;
76
+ color: ${accentColor};
77
+ letter-spacing: -1px;
78
+ margin-bottom: 40px;
79
+ max-width: 900px;
80
+ word-break: break-word;
81
+ padding: 0 20px;
82
+ }
83
+
84
+ .cover-container::before {
85
+ content: '';
86
+ position: absolute;
87
+ top: 0; left: 0; right: 0; height: 15px;
88
+ background: ${accentColor};
89
+ }
90
+
91
+ .cover-decoration-bottom {
92
+ width: 120px;
93
+ height: 2px;
94
+ background: ${accentColor}33;
95
+ margin-bottom: 40px;
96
+ }
97
+
98
+ .cover-author {
99
+ font-size: 28px;
100
+ color: #888;
101
+ font-weight: 400;
102
+ letter-spacing: 2px;
103
+ }
104
+
105
+ .cover-image {
106
+ width: 100%;
107
+ max-width: 700px;
108
+ margin-bottom: 50px;
109
+ border-radius: 16px;
110
+ overflow: hidden;
111
+ }
112
+
113
+ .cover-image img {
114
+ width: 100%;
115
+ height: auto;
116
+ display: block;
117
+ object-fit: cover;
118
+ max-height: 500px;
119
+ }
120
+
121
+ .page-indicator {
122
+ position: absolute;
123
+ bottom: 50px;
124
+ right: 80px;
125
+ font-size: 22px;
126
+ color: #ccc;
127
+ font-weight: 300;
128
+ }
129
+ </style>
130
+ </head>
131
+ <body>
132
+ <div class="cover-container">
133
+ <div class="cover-decoration-top"></div>
134
+ ${imageBlock}
135
+ <div class="cover-title">${escapeHTML(title)}</div>
136
+ <div class="cover-decoration-bottom"></div>
137
+ ${author ? `<div class="cover-author">${escapeHTML(author)}</div>` : ''}
138
+ </div>
139
+ <div class="page-indicator">1 / ${totalPages}</div>
140
+ </body>
141
+ </html>`;
142
+ }
143
+
144
+ /**
145
+ * Generate the HTML for a content page.
146
+ *
147
+ * @param {string} contentHTML - Pre-rendered HTML content for this page
148
+ * @param {number} pageNum - Current page number (1-based)
149
+ * @param {number} totalPages - Total page count
150
+ * @param {string} title - Article title (shown as header)
151
+ * @param {string} accentColor - Accent color hex
152
+ * @param {string} bgColor - Background color hex
153
+ * @returns {string} Complete HTML string
154
+ */
155
+ function buildContentPageHTML(contentHTML, pageNum, totalPages, title, accentColor, bgColor) {
156
+ return `<!DOCTYPE html>
157
+ <html>
158
+ <head>
159
+ <meta charset="utf-8">
160
+ <style>
161
+ ${getBaseStyles(bgColor, accentColor)}
162
+
163
+ .page-header {
164
+ padding: 60px 80px 0 80px;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 16px;
168
+ }
169
+
170
+ .page-header-line {
171
+ width: 30px;
172
+ height: 3px;
173
+ background: ${accentColor};
174
+ border-radius: 2px;
175
+ flex-shrink: 0;
176
+ }
177
+
178
+ .page-header-title {
179
+ font-size: 20px;
180
+ font-weight: 600;
181
+ color: #bbb;
182
+ letter-spacing: 1px;
183
+ white-space: nowrap;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ }
187
+
188
+ .content-area {
189
+ padding: 40px 80px 80px 80px;
190
+ flex: 1;
191
+ }
192
+
193
+ .content-area h2 {
194
+ font-size: 42px;
195
+ font-weight: 800;
196
+ color: ${accentColor};
197
+ margin: 0 0 28px 0;
198
+ line-height: 1.35;
199
+ letter-spacing: -0.5px;
200
+ }
201
+
202
+ .content-area h3 {
203
+ font-size: 34px;
204
+ font-weight: 700;
205
+ color: ${accentColor};
206
+ margin: 0 0 22px 0;
207
+ line-height: 1.35;
208
+ }
209
+
210
+ .content-area p {
211
+ font-size: 30px;
212
+ line-height: 1.75;
213
+ color: #333;
214
+ margin: 0 0 24px 0;
215
+ font-weight: 400;
216
+ }
217
+
218
+ .content-area ul, .content-area ol {
219
+ padding-left: 40px;
220
+ margin: 0 0 24px 0;
221
+ }
222
+
223
+ .content-area li {
224
+ font-size: 30px;
225
+ line-height: 1.7;
226
+ color: #333;
227
+ margin-bottom: 12px;
228
+ }
229
+
230
+ .content-area strong {
231
+ font-weight: 700;
232
+ color: ${accentColor};
233
+ }
234
+
235
+ .content-area em {
236
+ font-style: italic;
237
+ color: #555;
238
+ }
239
+
240
+ .content-area blockquote {
241
+ border-left: 4px solid ${accentColor}44;
242
+ margin: 0 0 24px 0;
243
+ padding: 16px 24px;
244
+ background: ${accentColor}08;
245
+ border-radius: 0 12px 12px 0;
246
+ }
247
+
248
+ .content-area blockquote p {
249
+ font-size: 28px;
250
+ color: #555;
251
+ margin: 0;
252
+ font-style: italic;
253
+ }
254
+
255
+ .content-area img {
256
+ max-width: 100%;
257
+ border-radius: 12px;
258
+ margin: 16px 0;
259
+ }
260
+
261
+ .content-area code {
262
+ background: #f0f0f0;
263
+ padding: 2px 8px;
264
+ border-radius: 4px;
265
+ font-size: 26px;
266
+ font-family: 'SF Mono', 'Menlo', monospace;
267
+ }
268
+
269
+ .page-indicator {
270
+ position: absolute;
271
+ bottom: 50px;
272
+ right: 80px;
273
+ font-size: 22px;
274
+ color: #ccc;
275
+ font-weight: 300;
276
+ }
277
+
278
+ .page-footer-decoration {
279
+ position: absolute;
280
+ bottom: 45px;
281
+ left: 80px;
282
+ width: 40px;
283
+ height: 3px;
284
+ background: ${accentColor}22;
285
+ border-radius: 2px;
286
+ }
287
+ </style>
288
+ </head>
289
+ <body>
290
+ <div class="page-header">
291
+ <div class="page-header-line"></div>
292
+ <div class="page-header-title">${escapeHTML(title)}</div>
293
+ </div>
294
+ <div class="content-area">
295
+ ${contentHTML}
296
+ </div>
297
+ <div class="page-footer-decoration"></div>
298
+ <div class="page-indicator">${pageNum} / ${totalPages}</div>
299
+ </body>
300
+ </html>`;
301
+ }
302
+
303
+ /**
304
+ * Shared base CSS styles for all pages.
305
+ *
306
+ * @param {string} bgColor - Background color
307
+ * @param {string} accentColor - Accent/text color
308
+ * @returns {string} CSS string
309
+ */
310
+ function getBaseStyles(bgColor, accentColor) {
311
+ return `
312
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;800;900&display=swap');
313
+
314
+ * {
315
+ margin: 0;
316
+ padding: 0;
317
+ box-sizing: border-box;
318
+ }
319
+
320
+ body {
321
+ width: ${WIDTH}px;
322
+ height: ${HEIGHT}px;
323
+ background: ${bgColor};
324
+ font-family: 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif;
325
+ overflow: hidden;
326
+ position: relative;
327
+ }
328
+ `;
329
+ }
330
+
331
+ /**
332
+ * Escape HTML special characters.
333
+ *
334
+ * @param {string} str - Raw string
335
+ * @returns {string} Escaped HTML string
336
+ */
337
+ function escapeHTML(str) {
338
+ return str
339
+ .replace(/&/g, '&amp;')
340
+ .replace(/</g, '&lt;')
341
+ .replace(/>/g, '&gt;')
342
+ .replace(/"/g, '&quot;');
343
+ }
344
+
345
+ /**
346
+ * Simple markdown to HTML converter.
347
+ * Handles: h1-h3, bold, italic, lists, blockquotes, images, links, code, paragraphs.
348
+ *
349
+ * @param {string} md - Markdown text
350
+ * @returns {string} HTML string
351
+ */
352
+ function markdownToHTML(md) {
353
+ let html = md;
354
+
355
+ // Images: ![alt](url)
356
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
357
+
358
+ // Links: [text](url) → just text (no clickable links in images)
359
+ html = html.replace(/\[([^\]]+)\]\([^)]+\)/g, '<strong>$1</strong>');
360
+
361
+ // Bold: **text** or __text__
362
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
363
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
364
+
365
+ // Italic: *text* or _text_
366
+ html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
367
+ html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '<em>$1</em>');
368
+
369
+ // Inline code: `code`
370
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
371
+
372
+ // Process line by line
373
+ const lines = html.split('\n');
374
+ const result = [];
375
+ let inList = false;
376
+ let listType = '';
377
+ let inBlockquote = false;
378
+ let blockquoteLines = [];
379
+
380
+ for (let i = 0; i < lines.length; i++) {
381
+ const line = lines[i];
382
+ const trimmed = line.trim();
383
+
384
+ // End blockquote
385
+ if (inBlockquote && !trimmed.startsWith('>')) {
386
+ result.push(`<blockquote><p>${blockquoteLines.join('<br/>')}</p></blockquote>`);
387
+ blockquoteLines = [];
388
+ inBlockquote = false;
389
+ }
390
+
391
+ // End list
392
+ if (inList && !trimmed.match(/^[-*]\s/) && !trimmed.match(/^\d+\.\s/) && trimmed !== '') {
393
+ result.push(listType === 'ul' ? '</ul>' : '</ol>');
394
+ inList = false;
395
+ }
396
+
397
+ // Skip empty lines
398
+ if (trimmed === '') {
399
+ if (inList) {
400
+ result.push(listType === 'ul' ? '</ul>' : '</ol>');
401
+ inList = false;
402
+ }
403
+ continue;
404
+ }
405
+
406
+ // Headers (skip h1 — it's the title, shown on cover)
407
+ if (trimmed.startsWith('### ')) {
408
+ result.push(`<h3>${trimmed.slice(4)}</h3>`);
409
+ } else if (trimmed.startsWith('## ')) {
410
+ result.push(`<h2>${trimmed.slice(3)}</h2>`);
411
+ } else if (trimmed.startsWith('# ')) {
412
+ // Skip h1 (title page handles it)
413
+ continue;
414
+ }
415
+ // Blockquote
416
+ else if (trimmed.startsWith('>')) {
417
+ inBlockquote = true;
418
+ blockquoteLines.push(trimmed.slice(1).trim());
419
+ }
420
+ // Unordered list
421
+ else if (trimmed.match(/^[-*]\s/)) {
422
+ if (!inList || listType !== 'ul') {
423
+ if (inList) result.push(listType === 'ul' ? '</ul>' : '</ol>');
424
+ result.push('<ul>');
425
+ inList = true;
426
+ listType = 'ul';
427
+ }
428
+ result.push(`<li>${trimmed.slice(2)}</li>`);
429
+ }
430
+ // Ordered list
431
+ else if (trimmed.match(/^\d+\.\s/)) {
432
+ if (!inList || listType !== 'ol') {
433
+ if (inList) result.push(listType === 'ul' ? '</ul>' : '</ol>');
434
+ result.push('<ol>');
435
+ inList = true;
436
+ listType = 'ol';
437
+ }
438
+ result.push(`<li>${trimmed.replace(/^\d+\.\s/, '')}</li>`);
439
+ }
440
+ // Horizontal rule → page break hint (handled at split level)
441
+ else if (trimmed.match(/^[-*_]{3,}$/)) {
442
+ result.push('<hr/>');
443
+ }
444
+ // Paragraph
445
+ else {
446
+ result.push(`<p>${trimmed}</p>`);
447
+ }
448
+ }
449
+
450
+ // Close any open structures
451
+ if (inBlockquote) {
452
+ result.push(`<blockquote><p>${blockquoteLines.join('<br/>')}</p></blockquote>`);
453
+ }
454
+ if (inList) {
455
+ result.push(listType === 'ul' ? '</ul>' : '</ol>');
456
+ }
457
+
458
+ return result.join('\n');
459
+ }
460
+
461
+ /**
462
+ * Split markdown content into pages.
463
+ * Splits on --- (horizontal rules) first, then by approximate character count.
464
+ *
465
+ * @param {string} markdown - Full markdown (without title line)
466
+ * @param {number} maxChars - Max chars per page
467
+ * @returns {string[]} Array of markdown chunks, one per page
468
+ */
469
+ function splitIntoPages(markdown, maxChars) {
470
+ // First split on explicit --- page breaks
471
+ const explicitSections = markdown.split(/\n---\n/).filter((s) => s.trim());
472
+
473
+ const pages = [];
474
+
475
+ for (const section of explicitSections) {
476
+ // If section fits, keep as one page
477
+ if (section.length <= maxChars) {
478
+ pages.push(section.trim());
479
+ continue;
480
+ }
481
+
482
+ // Split further by paragraphs (double newline)
483
+ const blocks = section.split(/\n\n/).filter((b) => b.trim());
484
+ let currentPage = '';
485
+
486
+ for (const block of blocks) {
487
+ // If adding this block exceeds limit, start a new page
488
+ if (currentPage.length > 0 && currentPage.length + block.length > maxChars) {
489
+ pages.push(currentPage.trim());
490
+ currentPage = block;
491
+ } else {
492
+ currentPage += (currentPage ? '\n\n' : '') + block;
493
+ }
494
+ }
495
+
496
+ if (currentPage.trim()) {
497
+ pages.push(currentPage.trim());
498
+ }
499
+ }
500
+
501
+ return pages.length > 0 ? pages : [markdown.trim()];
502
+ }
503
+
504
+ /**
505
+ * Extract the title from markdown content (first # heading).
506
+ *
507
+ * @param {string} markdown - Full markdown content
508
+ * @returns {{ title: string, body: string }} Extracted title and remaining body
509
+ */
510
+ function extractTitle(markdown) {
511
+ const lines = markdown.split('\n');
512
+ let title = '';
513
+ let bodyStart = 0;
514
+
515
+ for (let i = 0; i < lines.length; i++) {
516
+ const trimmed = lines[i].trim();
517
+ if (trimmed.startsWith('# ') && !trimmed.startsWith('## ')) {
518
+ title = trimmed.slice(2).trim();
519
+ bodyStart = i + 1;
520
+ break;
521
+ }
522
+ if (trimmed !== '') {
523
+ // No h1 found before content — use first non-empty line
524
+ break;
525
+ }
526
+ }
527
+
528
+ const body = lines.slice(bodyStart).join('\n').trim();
529
+ return { title, body };
530
+ }
531
+
532
+ // ─── Main ───────────────────────────────────────────────────────────────────
533
+
534
+ async function main() {
535
+ const totalContentPages = pages.length;
536
+ const totalPages = totalContentPages + 1; // +1 for cover
537
+
538
+ const browser = await chromium.launch({ headless: true });
539
+ const context = await browser.newContext({
540
+ viewport: { width: WIDTH, height: HEIGHT },
541
+ deviceScaleFactor: 2, // Retina for crisp text
542
+ });
543
+
544
+ const filePaths = [];
545
+
546
+ try {
547
+ // Page 1: Cover
548
+ const coverHTML = buildCoverHTML(title, author, accentColor, bgColor, coverImage, totalPages);
549
+ const coverPage = await context.newPage();
550
+ await coverPage.setContent(coverHTML, { waitUntil: 'networkidle' });
551
+ // Wait for fonts to load
552
+ await coverPage.waitForTimeout(500);
553
+ const coverPath = join(outputDir, 'page-01.png');
554
+ await coverPage.screenshot({ path: coverPath, type: 'png' });
555
+ filePaths.push(coverPath);
556
+ await coverPage.close();
557
+
558
+ // Content pages
559
+ for (let i = 0; i < pages.length; i++) {
560
+ const pageNum = i + 2; // 1-based, after cover
561
+ const contentHTML = markdownToHTML(pages[i]);
562
+ const pageHTML = buildContentPageHTML(
563
+ contentHTML,
564
+ pageNum,
565
+ totalPages,
566
+ title,
567
+ accentColor,
568
+ bgColor
569
+ );
570
+
571
+ const contentPage = await context.newPage();
572
+ await contentPage.setContent(pageHTML, { waitUntil: 'networkidle' });
573
+ await contentPage.waitForTimeout(300);
574
+
575
+ const pagePath = join(outputDir, `page-${String(pageNum).padStart(2, '0')}.png`);
576
+ await contentPage.screenshot({ path: pagePath, type: 'png' });
577
+ filePaths.push(pagePath);
578
+ await contentPage.close();
579
+ }
580
+ } finally {
581
+ await browser.close();
582
+ }
583
+
584
+ return {
585
+ success: true,
586
+ totalPages,
587
+ files: filePaths,
588
+ dimensions: { width: WIDTH, height: HEIGHT },
589
+ outputDir: resolve(outputDir),
590
+ };
591
+ }
592
+
593
+ main()
594
+ .then((result) => {
595
+ console.log(JSON.stringify(result));
596
+ process.exit(0);
597
+ })
598
+ .catch((err) => {
599
+ console.error(JSON.stringify({ success: false, error: err.message }));
600
+ process.exit(1);
601
+ });
@@ -31,8 +31,14 @@ export declare class CommunicationModule implements PromptModule {
31
31
  build(config: ModuleConfig): Promise<string>;
32
32
  /**
33
33
  * Build a skill call example snippet. For gemini-cli runtime, uses --file
34
- * pattern to avoid shell escaping EOF errors. For other runtimes, uses
35
- * inline JSON argument.
34
+ * pattern with heredoc to avoid shell escaping EOF errors. For other runtimes,
35
+ * uses inline JSON argument.
36
+ *
37
+ * Uses heredoc with single-quoted delimiter (`<< 'CREWLY_EOF'`) instead of
38
+ * `printf '%s' '...'` because heredoc prevents ALL shell interpretation —
39
+ * single quotes, double quotes, backticks, $variables, and parentheses all
40
+ * pass through literally. The previous printf approach broke when JSON
41
+ * contained single quotes (e.g., "it's working").
36
42
  *
37
43
  * @param config - Module configuration with runtime type
38
44
  * @param skillPath - Relative path to the skill (e.g., 'core/report-status')
@@ -1 +1 @@
1
- {"version":3,"file":"communication.module.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/communication.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAoB,MAAM,8BAA8B,CAAC;AAE5F;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACvD,IAAI,SAAmB;IACvB,QAAQ,SAAK;IACb,SAAS,SAAQ;IACjB,WAAW,UAAQ;IAEnB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO;IAI7C;;;;;;;OAOG;IACG,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBlD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,iBAAiB;IAazB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAyE9B;;OAEG;IACH,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAqBxB"}
1
+ {"version":3,"file":"communication.module.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/communication.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAoB,MAAM,8BAA8B,CAAC;AAE5F;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,YAAW,YAAY;IACvD,IAAI,SAAmB;IACvB,QAAQ,SAAK;IACb,SAAS,SAAQ;IACjB,WAAW,UAAQ;IAEnB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO;IAI7C;;;;;;;OAOG;IACG,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBlD;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAyE9B;;OAEG;IACH,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAqBxB"}
@@ -48,8 +48,14 @@ export class CommunicationModule {
48
48
  }
49
49
  /**
50
50
  * Build a skill call example snippet. For gemini-cli runtime, uses --file
51
- * pattern to avoid shell escaping EOF errors. For other runtimes, uses
52
- * inline JSON argument.
51
+ * pattern with heredoc to avoid shell escaping EOF errors. For other runtimes,
52
+ * uses inline JSON argument.
53
+ *
54
+ * Uses heredoc with single-quoted delimiter (`<< 'CREWLY_EOF'`) instead of
55
+ * `printf '%s' '...'` because heredoc prevents ALL shell interpretation —
56
+ * single quotes, double quotes, backticks, $variables, and parentheses all
57
+ * pass through literally. The previous printf approach broke when JSON
58
+ * contained single quotes (e.g., "it's working").
53
59
  *
54
60
  * @param config - Module configuration with runtime type
55
61
  * @param skillPath - Relative path to the skill (e.g., 'core/report-status')
@@ -62,7 +68,9 @@ export class CommunicationModule {
62
68
  const resolvedBase = basePath || config.agentSkillsPath;
63
69
  if (config.runtimeType === 'gemini-cli') {
64
70
  return `\`\`\`bash
65
- printf '%s' '${jsonExample}' > /tmp/crewly_skill_input.json
71
+ cat > /tmp/crewly_skill_input.json << 'CREWLY_EOF'
72
+ ${jsonExample}
73
+ CREWLY_EOF
66
74
  bash ${resolvedBase}/${skillPath}/execute.sh --file /tmp/crewly_skill_input.json
67
75
  \`\`\``;
68
76
  }
@@ -1 +1 @@
1
- {"version":3,"file":"communication.module.js","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/communication.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"communication.module.js","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/communication.module.ts"],"names":[],"mappings":"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"}
@@ -39,8 +39,14 @@ export declare class SkillsReferenceModule implements PromptModule {
39
39
  *
40
40
  * Gemini CLI's run_shell_command mangles JSON arguments containing quotes,
41
41
  * backticks, and parentheses, causing "unexpected EOF" shell errors.
42
- * This guide instructs the agent to write JSON to a temp file first,
43
- * then pass --file <path> to avoid shell interpretation entirely.
42
+ * This guide instructs the agent to write JSON to a temp file using heredoc
43
+ * with a single-quoted delimiter, then pass --file <path>.
44
+ *
45
+ * Uses `<< 'CREWLY_EOF'` instead of `printf '%s' '...'` because:
46
+ * - Single-quoted heredoc delimiter prevents ALL shell interpretation
47
+ * - Single quotes, double quotes, backticks, $, () all pass through literally
48
+ * - The previous printf approach broke when JSON contained single quotes
49
+ * (e.g., "it's working" or "don't forget")
44
50
  *
45
51
  * Only included for gemini-cli runtime type.
46
52
  *
@@ -1 +1 @@
1
- {"version":3,"file":"skills-reference.module.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/skills-reference.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;;;;GAQG;AACH,qBAAa,qBAAsB,YAAW,YAAY;IACzD,IAAI,SAAuB;IAC3B,QAAQ,SAAK;IACb,SAAS,SAAO;IAChB,WAAW,UAAS;IAEpB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO;IAI7C;;;;;;OAMG;IACG,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAalD;;OAEG;IACH,OAAO,CAAC,eAAe;IAoDvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAiCzB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,kBAAkB;IAiC1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;CAkB1B"}
1
+ {"version":3,"file":"skills-reference.module.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/skills-reference.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;;;;GAQG;AACH,qBAAa,qBAAsB,YAAW,YAAY;IACzD,IAAI,SAAuB;IAC3B,QAAQ,SAAK;IACb,SAAS,SAAO;IAChB,WAAW,UAAS;IAEpB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO;IAI7C;;;;;;OAMG;IACG,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAalD;;OAEG;IACH,OAAO,CAAC,eAAe;IAoDvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAiCzB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,kBAAkB;IAsC1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;CAkB1B"}
@@ -84,8 +84,14 @@ export class SkillsReferenceModule {
84
84
  *
85
85
  * Gemini CLI's run_shell_command mangles JSON arguments containing quotes,
86
86
  * backticks, and parentheses, causing "unexpected EOF" shell errors.
87
- * This guide instructs the agent to write JSON to a temp file first,
88
- * then pass --file <path> to avoid shell interpretation entirely.
87
+ * This guide instructs the agent to write JSON to a temp file using heredoc
88
+ * with a single-quoted delimiter, then pass --file <path>.
89
+ *
90
+ * Uses `<< 'CREWLY_EOF'` instead of `printf '%s' '...'` because:
91
+ * - Single-quoted heredoc delimiter prevents ALL shell interpretation
92
+ * - Single quotes, double quotes, backticks, $, () all pass through literally
93
+ * - The previous printf approach broke when JSON contained single quotes
94
+ * (e.g., "it's working" or "don't forget")
89
95
  *
90
96
  * Only included for gemini-cli runtime type.
91
97
  *
@@ -98,13 +104,15 @@ export class SkillsReferenceModule {
98
104
  }
99
105
  return `## Safe Skill Calling (MANDATORY)
100
106
 
101
- **CRITICAL:** When calling any bash skill, you MUST use the \`--file\` pattern to avoid shell escaping errors.
107
+ **CRITICAL:** When calling any bash skill, you MUST use the \`--file\` pattern with heredoc to avoid shell escaping errors.
102
108
  Passing JSON directly as a shell argument will cause "unexpected EOF" errors when the content contains quotes, backticks, or parentheses.
103
109
 
104
110
  ### Required Pattern
105
111
  \`\`\`bash
106
- # Step 1: Write JSON to a temp file (printf preserves all special characters)
107
- printf '%s' '{"sessionName":"my-agent","status":"done","summary":"Fixed the bug"}' > /tmp/crewly_skill_input.json
112
+ # Step 1: Write JSON to a temp file using heredoc (single-quoted delimiter prevents ALL shell interpretation)
113
+ cat > /tmp/crewly_skill_input.json << 'CREWLY_EOF'
114
+ {"sessionName":"my-agent","status":"done","summary":"Fixed the bug — it's working now"}
115
+ CREWLY_EOF
108
116
 
109
117
  # Step 2: Call the skill with --file flag
110
118
  bash ${config.agentSkillsPath}/core/report-status/execute.sh --file /tmp/crewly_skill_input.json
@@ -112,16 +120,19 @@ bash ${config.agentSkillsPath}/core/report-status/execute.sh --file /tmp/crewly_
112
120
 
113
121
  ### Team Leader Skills (delegate-task, verify-output, schedule-check)
114
122
  \`\`\`bash
115
- printf '%s' '{"to":"worker-session","task":"implement feature"}' > /tmp/crewly_skill_input.json
123
+ cat > /tmp/crewly_skill_input.json << 'CREWLY_EOF'
124
+ {"to":"worker-session","task":"implement feature","priority":"high"}
125
+ CREWLY_EOF
116
126
  bash ${config.tlSkillsPath}/delegate-task/execute.sh --file /tmp/crewly_skill_input.json
117
127
  \`\`\`` : ''}
118
128
 
119
129
  ### Rules
120
- 1. **ALWAYS** use \`printf '%s' '<json>' > /tmp/crewly_skill_input.json\` then \`--file\`
121
- 2. **NEVER** pass JSON directly as a shell argument: \`bash execute.sh '{"key":"value with 'quotes'"}'\`
122
- 3. Use \`/tmp/crewly_skill_input.json\` as the temp file (overwritten each call)
123
- 4. This applies to ALL skills: report-status, send-message, remember, recall, delegate-task, reply-slack, reply-gchat, etc.
124
- 5. For skills that support named flags (reply-slack, reply-gchat), you may also use \`--text-file\` for the message body`;
130
+ 1. **ALWAYS** use heredoc to write JSON: \`cat > /tmp/crewly_skill_input.json << 'CREWLY_EOF'\` then the JSON, then \`CREWLY_EOF\` on its own line
131
+ 2. **NEVER** pass JSON directly as a shell argument: \`bash execute.sh '{"key":"value"}'\`
132
+ 3. **NEVER** use \`printf '%s' '...'\` single quotes inside JSON will break it
133
+ 4. Use \`/tmp/crewly_skill_input.json\` as the temp file (overwritten each call)
134
+ 5. This applies to ALL skills: report-status, send-message, remember, recall, delegate-task, reply-slack, reply-gchat, etc.
135
+ 6. For skills that support named flags (reply-slack, reply-gchat), you may also use \`--text-file\` for the message body`;
125
136
  }
126
137
  /**
127
138
  * Build communication and memory tool instructions.
@@ -1 +1 @@
1
- {"version":3,"file":"skills-reference.module.js","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/skills-reference.module.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"skills-reference.module.js","sourceRoot":"","sources":["../../../../../../../backend/src/services/ai/prompt-modules/skills-reference.module.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewly",
3
- "version": "1.5.14",
3
+ "version": "1.5.15",
4
4
  "type": "module",
5
5
  "description": "Multi-agent orchestration platform for AI coding teams — coordinates Claude Code, Gemini CLI, and Codex agents with a real-time web dashboard",
6
6
  "main": "dist/cli/cli/src/index.js",