fraim-framework 2.0.34 → 2.0.35

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.
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Markdown to PDF Converter Script
5
+ *
6
+ * This script converts markdown files to PDF using puppeteer and markdown-it.
7
+ * It supports various markdown features including tables, code blocks, and images.
8
+ *
9
+ * EXECUTION MODEL:
10
+ * - Script location: ~/.fraim/scripts/markdown-to-pdf.js
11
+ * - Working directory: Current project directory (process.cwd())
12
+ * - Input/output files: Relative to current project directory
13
+ * - Config: Reads from .fraim/config.json in current project directory
14
+ *
15
+ * Usage:
16
+ * node ~/.fraim/scripts/markdown-to-pdf.js <input.md> [output.pdf] [options]
17
+ *
18
+ * Options:
19
+ * --format <format> Paper format (A4, Letter, Legal, etc.) - default: A4
20
+ * --margin <margin> Page margins in inches - default: 0.5
21
+ * --css <file> Custom CSS file for styling
22
+ * --header <text> Header text
23
+ * --footer <text> Footer text
24
+ * --landscape Use landscape orientation
25
+ * --no-background Disable background graphics
26
+ *
27
+ * Examples:
28
+ * node ~/.fraim/scripts/markdown-to-pdf.js README.md
29
+ * node ~/.fraim/scripts/markdown-to-pdf.js docs/spec.md output/spec.pdf --format Letter
30
+ * node ~/.fraim/scripts/markdown-to-pdf.js report.md --css custom.css --header "Company Report"
31
+ */
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ // Get project directory (where the user is working)
37
+ const PROJECT_DIR = process.cwd();
38
+ const CONFIG_FILE = path.join(PROJECT_DIR, '.fraim', 'config.json');
39
+
40
+ // Load project configuration if available
41
+ function loadProjectConfig() {
42
+ try {
43
+ if (fs.existsSync(CONFIG_FILE)) {
44
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
45
+ return config;
46
+ }
47
+ } catch (error) {
48
+ console.warn('Warning: Could not load project config from .fraim/config.json');
49
+ }
50
+ return {};
51
+ }
52
+
53
+ // Check if required dependencies are available
54
+ function checkDependencies() {
55
+ const requiredPackages = ['puppeteer', 'markdown-it', 'markdown-it-highlightjs'];
56
+ const missingPackages = [];
57
+
58
+ for (const pkg of requiredPackages) {
59
+ try {
60
+ require.resolve(pkg);
61
+ } catch (error) {
62
+ missingPackages.push(pkg);
63
+ }
64
+ }
65
+
66
+ if (missingPackages.length > 0) {
67
+ console.error('Missing required packages:', missingPackages.join(', '));
68
+ console.error('Install them with: npm install', missingPackages.join(' '));
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ // Parse command line arguments
74
+ function parseArgs() {
75
+ const args = process.argv.slice(2);
76
+
77
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
78
+ console.log(`
79
+ Markdown to PDF Converter
80
+
81
+ Usage: node ~/.fraim/scripts/markdown-to-pdf.js <input.md> [output.pdf] [options]
82
+
83
+ Options:
84
+ --format <format> Paper format (A4, Letter, Legal, etc.) - default: A4
85
+ --margin <margin> Page margins in inches - default: 0.5
86
+ --css <file> Custom CSS file for styling
87
+ --header <text> Header text
88
+ --footer <text> Footer text
89
+ --landscape Use landscape orientation
90
+ --no-background Disable background graphics
91
+ --help, -h Show this help message
92
+
93
+ Examples:
94
+ node ~/.fraim/scripts/markdown-to-pdf.js README.md
95
+ node ~/.fraim/scripts/markdown-to-pdf.js docs/spec.md output/spec.pdf --format Letter
96
+ node ~/.fraim/scripts/markdown-to-pdf.js report.md --css custom.css --header "Company Report"
97
+
98
+ Working Directory: ${PROJECT_DIR}
99
+ Config File: ${CONFIG_FILE}
100
+ `);
101
+ process.exit(0);
102
+ }
103
+
104
+ const config = {
105
+ input: args[0],
106
+ output: null,
107
+ format: 'A4',
108
+ margin: '0.5in',
109
+ css: null,
110
+ header: null,
111
+ footer: null,
112
+ landscape: false,
113
+ background: true
114
+ };
115
+
116
+ // Resolve input path relative to project directory
117
+ if (!path.isAbsolute(config.input)) {
118
+ config.input = path.resolve(PROJECT_DIR, config.input);
119
+ }
120
+
121
+ // Check if second argument is output file or option
122
+ if (args[1] && !args[1].startsWith('--')) {
123
+ config.output = args[1];
124
+ }
125
+
126
+ // Parse options
127
+ for (let i = 0; i < args.length; i++) {
128
+ const arg = args[i];
129
+
130
+ if (arg === '--format' && args[i + 1]) {
131
+ config.format = args[i + 1];
132
+ i++;
133
+ } else if (arg === '--margin' && args[i + 1]) {
134
+ config.margin = args[i + 1];
135
+ i++;
136
+ } else if (arg === '--css' && args[i + 1]) {
137
+ config.css = args[i + 1];
138
+ i++;
139
+ } else if (arg === '--header' && args[i + 1]) {
140
+ config.header = args[i + 1];
141
+ i++;
142
+ } else if (arg === '--footer' && args[i + 1]) {
143
+ config.footer = args[i + 1];
144
+ i++;
145
+ } else if (arg === '--landscape') {
146
+ config.landscape = true;
147
+ } else if (arg === '--no-background') {
148
+ config.background = false;
149
+ }
150
+ }
151
+
152
+ // Set default output if not provided
153
+ if (!config.output) {
154
+ const inputPath = path.parse(config.input);
155
+ config.output = path.join(inputPath.dir, inputPath.name + '.pdf');
156
+ }
157
+
158
+ // Resolve output path relative to project directory
159
+ if (!path.isAbsolute(config.output)) {
160
+ config.output = path.resolve(PROJECT_DIR, config.output);
161
+ }
162
+
163
+ // Resolve CSS path relative to project directory if provided
164
+ if (config.css && !path.isAbsolute(config.css)) {
165
+ config.css = path.resolve(PROJECT_DIR, config.css);
166
+ }
167
+
168
+ return config;
169
+ }
170
+
171
+ // Convert markdown to HTML
172
+ async function markdownToHtml(markdownContent, customCss = null) {
173
+ const MarkdownIt = require('markdown-it');
174
+ const hljs = require('markdown-it-highlightjs');
175
+
176
+ const md = new MarkdownIt({
177
+ html: true,
178
+ linkify: true,
179
+ typographer: true,
180
+ breaks: false
181
+ }).use(hljs);
182
+
183
+ const htmlContent = md.render(markdownContent);
184
+
185
+ // Default CSS for better PDF rendering
186
+ const defaultCss = `
187
+ body {
188
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
189
+ line-height: 1.6;
190
+ color: #333;
191
+ max-width: none;
192
+ margin: 0;
193
+ padding: 20px;
194
+ }
195
+
196
+ h1, h2, h3, h4, h5, h6 {
197
+ color: #2c3e50;
198
+ margin-top: 2em;
199
+ margin-bottom: 1em;
200
+ }
201
+
202
+ h1 { font-size: 2.5em; border-bottom: 2px solid #3498db; padding-bottom: 0.3em; }
203
+ h2 { font-size: 2em; border-bottom: 1px solid #bdc3c7; padding-bottom: 0.3em; }
204
+ h3 { font-size: 1.5em; }
205
+
206
+ code {
207
+ background-color: #f8f9fa;
208
+ padding: 2px 4px;
209
+ border-radius: 3px;
210
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
211
+ }
212
+
213
+ pre {
214
+ background-color: #f8f9fa;
215
+ border: 1px solid #e9ecef;
216
+ border-radius: 6px;
217
+ padding: 16px;
218
+ overflow-x: auto;
219
+ margin: 1em 0;
220
+ }
221
+
222
+ pre code {
223
+ background: none;
224
+ padding: 0;
225
+ }
226
+
227
+ table {
228
+ border-collapse: collapse;
229
+ width: 100%;
230
+ margin: 1em 0;
231
+ }
232
+
233
+ th, td {
234
+ border: 1px solid #ddd;
235
+ padding: 12px;
236
+ text-align: left;
237
+ }
238
+
239
+ th {
240
+ background-color: #f2f2f2;
241
+ font-weight: bold;
242
+ }
243
+
244
+ blockquote {
245
+ border-left: 4px solid #3498db;
246
+ margin: 1em 0;
247
+ padding-left: 1em;
248
+ color: #7f8c8d;
249
+ }
250
+
251
+ img {
252
+ max-width: 100%;
253
+ height: auto;
254
+ }
255
+
256
+ a {
257
+ color: #3498db;
258
+ text-decoration: none;
259
+ }
260
+
261
+ a:hover {
262
+ text-decoration: underline;
263
+ }
264
+
265
+ ul, ol {
266
+ padding-left: 2em;
267
+ }
268
+
269
+ li {
270
+ margin: 0.5em 0;
271
+ }
272
+
273
+ @media print {
274
+ body { margin: 0; }
275
+ h1, h2, h3, h4, h5, h6 { page-break-after: avoid; }
276
+ pre, blockquote { page-break-inside: avoid; }
277
+ img { page-break-inside: avoid; }
278
+ }
279
+ `;
280
+
281
+ let customCssContent = '';
282
+ if (customCss && fs.existsSync(customCss)) {
283
+ customCssContent = fs.readFileSync(customCss, 'utf8');
284
+ }
285
+
286
+ return `
287
+ <!DOCTYPE html>
288
+ <html>
289
+ <head>
290
+ <meta charset="utf-8">
291
+ <title>Converted from Markdown</title>
292
+ <style>${defaultCss}${customCssContent}</style>
293
+ </head>
294
+ <body>
295
+ ${htmlContent}
296
+ </body>
297
+ </html>
298
+ `;
299
+ }
300
+
301
+ // Convert HTML to PDF using Puppeteer
302
+ async function htmlToPdf(html, outputPath, config) {
303
+ const puppeteer = require('puppeteer');
304
+
305
+ const browser = await puppeteer.launch({
306
+ headless: 'new',
307
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
308
+ });
309
+
310
+ try {
311
+ const page = await browser.newPage();
312
+ await page.setContent(html, { waitUntil: 'networkidle0' });
313
+
314
+ const pdfOptions = {
315
+ path: outputPath,
316
+ format: config.format,
317
+ margin: {
318
+ top: config.margin,
319
+ right: config.margin,
320
+ bottom: config.margin,
321
+ left: config.margin
322
+ },
323
+ landscape: config.landscape,
324
+ printBackground: config.background,
325
+ preferCSSPageSize: true
326
+ };
327
+
328
+ if (config.header) {
329
+ pdfOptions.displayHeaderFooter = true;
330
+ pdfOptions.headerTemplate = `<div style="font-size: 10px; width: 100%; text-align: center;">${config.header}</div>`;
331
+ }
332
+
333
+ if (config.footer) {
334
+ pdfOptions.displayHeaderFooter = true;
335
+ pdfOptions.footerTemplate = `<div style="font-size: 10px; width: 100%; text-align: center;">${config.footer}</div>`;
336
+ }
337
+
338
+ await page.pdf(pdfOptions);
339
+ console.log(`✅ PDF generated successfully: ${path.relative(PROJECT_DIR, outputPath)}`);
340
+
341
+ } finally {
342
+ await browser.close();
343
+ }
344
+ }
345
+
346
+ // Main function
347
+ async function main() {
348
+ try {
349
+ checkDependencies();
350
+ const config = parseArgs();
351
+ const projectConfig = loadProjectConfig();
352
+
353
+ console.log(`📄 Converting markdown to PDF...`);
354
+ console.log(` Working directory: ${PROJECT_DIR}`);
355
+ console.log(` Input: ${path.relative(PROJECT_DIR, config.input)}`);
356
+ console.log(` Output: ${path.relative(PROJECT_DIR, config.output)}`);
357
+
358
+ // Validate input file
359
+ if (!fs.existsSync(config.input)) {
360
+ console.error(`❌ Input file not found: ${path.relative(PROJECT_DIR, config.input)}`);
361
+ process.exit(1);
362
+ }
363
+
364
+ // Ensure output directory exists
365
+ const outputDir = path.dirname(config.output);
366
+ if (!fs.existsSync(outputDir)) {
367
+ fs.mkdirSync(outputDir, { recursive: true });
368
+ console.log(`📁 Created output directory: ${path.relative(PROJECT_DIR, outputDir)}`);
369
+ }
370
+
371
+ // Read markdown file
372
+ const markdownContent = fs.readFileSync(config.input, 'utf8');
373
+
374
+ // Convert to HTML
375
+ const html = await markdownToHtml(markdownContent, config.css);
376
+
377
+ // Convert to PDF
378
+ await htmlToPdf(html, config.output, config);
379
+
380
+ } catch (error) {
381
+ console.error('❌ Error:', error.message);
382
+ process.exit(1);
383
+ }
384
+ }
385
+
386
+ // Run if called directly
387
+ if (require.main === module) {
388
+ main();
389
+ }
390
+
391
+ module.exports = { markdownToHtml, htmlToPdf, parseArgs };
@@ -134,6 +134,7 @@ try {
134
134
 
135
135
  // Support both 'repository' (new) and 'git' (legacy/current) schemas
136
136
  let repo = config.repository;
137
+ let defaultBranch = 'master'; // Default fallback
137
138
 
138
139
  if (!repo) {
139
140
  if (config.git) {
@@ -142,13 +143,18 @@ try {
142
143
  name: config.git.repoName,
143
144
  url: config.git.repoUrl || \`https://github.com/\${config.git.repoOwner}/\${config.git.repoName}.git\`
144
145
  };
146
+ // Extract defaultBranch from git config
147
+ defaultBranch = config.git.defaultBranch || 'master';
145
148
  }
149
+ } else {
150
+ // Extract defaultBranch from repository config
151
+ defaultBranch = repo.defaultBranch || 'master';
146
152
  }
147
153
 
148
154
  if (!repo || !repo.owner || !repo.name || !repo.url) {
149
155
  process.exit(1);
150
156
  }
151
- console.log(\`\${repo.owner}:\${repo.name}:\${repo.url}\`);
157
+ console.log(\`\${repo.owner}|\${repo.name}|\${repo.url}|\${defaultBranch}\`);
152
158
  } catch (e) {
153
159
  process.exit(1);
154
160
  }
@@ -161,13 +167,14 @@ if [ $? -ne 0 ]; then
161
167
  exit 1
162
168
  fi
163
169
 
164
- # Split the result into variables
165
- IFS=':' read -r REPO_OWNER REPO_NAME REPO_URL <<< "$REPO_INFO"
170
+ # Split the result into variables using pipe delimiter
171
+ IFS='|' read -r REPO_OWNER REPO_NAME REPO_URL CONFIG_DEFAULT_BRANCH <<< "$REPO_INFO"
166
172
 
167
173
  echo "Repository Configuration:"
168
174
  echo " Owner: $REPO_OWNER"
169
175
  echo " Name: $REPO_NAME"
170
176
  echo " URL: $REPO_URL"
177
+ echo " Default Branch: $CONFIG_DEFAULT_BRANCH"
171
178
  echo
172
179
 
173
180
  echo "=== $REPO_NAME - Issue Preparation ==="
@@ -195,45 +202,69 @@ echo "Step 1: Determining base branch from current repository..."
195
202
 
196
203
  # Determine base branch from the ORIGINAL repository (before cloning)
197
204
  if [ "$USE_DEFAULT_BRANCH" = true ]; then
198
- # Detect the default branch
199
- DEFAULT_BRANCH=""
200
- for candidate in main master develop; do
201
- if git ls-remote --exit-code --heads "$REPO_URL" "$candidate" >/dev/null 2>&1; then
202
- DEFAULT_BRANCH="$candidate"
203
- echo "✓ Detected default branch: $DEFAULT_BRANCH"
204
- break
205
- fi
206
- done
207
-
208
- # Fallback to master if nothing else worked
209
- if [ -z "$DEFAULT_BRANCH" ]; then
210
- DEFAULT_BRANCH="master"
211
- echo "⚠️ Could not detect default branch, defaulting to: $DEFAULT_BRANCH"
212
- fi
213
-
214
- BASE_BRANCH="$DEFAULT_BRANCH"
215
- echo "Using detected default branch as base: $BASE_BRANCH (--use-default flag)"
216
- else
217
- # Get current branch from the original repo
218
- ORIGINAL_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
205
+ # Use the default branch from FRAIM config
206
+ DEFAULT_BRANCH="$CONFIG_DEFAULT_BRANCH"
207
+ echo "✓ Using default branch from FRAIM config: $DEFAULT_BRANCH"
219
208
 
220
- if [ -z "$ORIGINAL_BRANCH" ]; then
221
- echo "Warning: Detached HEAD detected in original repo, falling back to default branch detection"
209
+ # Verify the branch exists on remote
210
+ if git ls-remote --exit-code --heads "$REPO_URL" "$DEFAULT_BRANCH" >/dev/null 2>&1; then
211
+ echo "✓ Confirmed branch '$DEFAULT_BRANCH' exists on remote"
212
+ else
213
+ echo "⚠️ Warning: Configured default branch '$DEFAULT_BRANCH' not found on remote"
214
+ echo " Falling back to branch detection..."
222
215
 
223
- # Detect the default branch
216
+ # Fallback to detection if configured branch doesn't exist
224
217
  DEFAULT_BRANCH=""
225
218
  for candidate in main master develop; do
226
219
  if git ls-remote --exit-code --heads "$REPO_URL" "$candidate" >/dev/null 2>&1; then
227
220
  DEFAULT_BRANCH="$candidate"
228
- echo "✓ Detected default branch: $DEFAULT_BRANCH"
221
+ echo "✓ Detected fallback branch: $DEFAULT_BRANCH"
229
222
  break
230
223
  fi
231
224
  done
232
225
 
233
- # Fallback to master if nothing else worked
226
+ # Final fallback to master
234
227
  if [ -z "$DEFAULT_BRANCH" ]; then
235
228
  DEFAULT_BRANCH="master"
236
- echo "⚠️ Could not detect default branch, defaulting to: $DEFAULT_BRANCH"
229
+ echo "⚠️ Could not detect any branch, defaulting to: $DEFAULT_BRANCH"
230
+ fi
231
+ fi
232
+
233
+ BASE_BRANCH="$DEFAULT_BRANCH"
234
+ echo "Using default branch as base: $BASE_BRANCH (--use-default flag)"
235
+ else
236
+ # Get current branch from the original repo
237
+ ORIGINAL_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
238
+
239
+ if [ -z "$ORIGINAL_BRANCH" ]; then
240
+ echo "Warning: Detached HEAD detected in original repo, falling back to configured default branch"
241
+
242
+ # Use the default branch from FRAIM config
243
+ DEFAULT_BRANCH="$CONFIG_DEFAULT_BRANCH"
244
+ echo "✓ Using default branch from FRAIM config: $DEFAULT_BRANCH"
245
+
246
+ # Verify the branch exists on remote
247
+ if git ls-remote --exit-code --heads "$REPO_URL" "$DEFAULT_BRANCH" >/dev/null 2>&1; then
248
+ echo "✓ Confirmed branch '$DEFAULT_BRANCH' exists on remote"
249
+ else
250
+ echo "⚠️ Warning: Configured default branch '$DEFAULT_BRANCH' not found on remote"
251
+ echo " Falling back to branch detection..."
252
+
253
+ # Fallback to detection if configured branch doesn't exist
254
+ DEFAULT_BRANCH=""
255
+ for candidate in main master develop; do
256
+ if git ls-remote --exit-code --heads "$REPO_URL" "$candidate" >/dev/null 2>&1; then
257
+ DEFAULT_BRANCH="$candidate"
258
+ echo "✓ Detected fallback branch: $DEFAULT_BRANCH"
259
+ break
260
+ fi
261
+ done
262
+
263
+ # Final fallback to master
264
+ if [ -z "$DEFAULT_BRANCH" ]; then
265
+ DEFAULT_BRANCH="master"
266
+ echo "⚠️ Could not detect any branch, defaulting to: $DEFAULT_BRANCH"
267
+ fi
237
268
  fi
238
269
 
239
270
  BASE_BRANCH="$DEFAULT_BRANCH"
@@ -0,0 +1,130 @@
1
+ # Template: Marketing Storytelling
2
+
3
+ Use this template to transform product development experiences into compelling founder narratives for events and speaking opportunities.
4
+
5
+ ## Project Setup
6
+ - **Product/Project Name**:
7
+ - **Issue Number**:
8
+ - **Target Event**:
9
+ - **Event Type**: [Conference Talk / Panel / Workshop / Podcast / Accelerator / Meetup]
10
+ - **Audience**: [Technical / Business / Mixed]
11
+ - **Event Date**:
12
+ - **Submission Deadline**:
13
+
14
+ ## Story Arc Development
15
+
16
+ ### Original Intent
17
+ **What was the vision?**
18
+
19
+
20
+ **Why did it matter?**
21
+
22
+
23
+ **What problem were you solving?**
24
+
25
+
26
+ ### Friction Points
27
+ **What unexpected challenges emerged?**
28
+
29
+
30
+ **What failures or setbacks occurred?**
31
+
32
+
33
+ **What assumptions proved wrong?**
34
+
35
+
36
+ ### Key Insights
37
+ **What "aha moments" happened?**
38
+
39
+
40
+ **What counterintuitive discoveries were made?**
41
+
42
+
43
+ **What would you do differently?**
44
+
45
+
46
+ ### Final Outcome
47
+ **What was actually built?**
48
+
49
+
50
+ **Why does it work?**
51
+
52
+
53
+ **What makes it different/better?**
54
+
55
+
56
+ ## Founder Lessons (3-5 key takeaways)
57
+
58
+ ### Lesson 1
59
+ **Lesson**:
60
+ **Context**:
61
+ **Application**:
62
+
63
+ ### Lesson 2
64
+ **Lesson**:
65
+ **Context**:
66
+ **Application**:
67
+
68
+ ### Lesson 3
69
+ **Lesson**:
70
+ **Context**:
71
+ **Application**:
72
+
73
+ ## Event Content
74
+
75
+ ### Session Title Options
76
+ 1.
77
+ 2.
78
+ 3.
79
+
80
+ ### Session Description (1-2 sentences)
81
+
82
+
83
+ ### Email Pitch (2-3 paragraphs)
84
+
85
+
86
+ ### Speaker Bio (50-100 words)
87
+
88
+
89
+ ### Presentation Outline
90
+
91
+ #### 5-Minute Version
92
+ - **Hook** (1 min):
93
+ - **Problem** (1 min):
94
+ - **Journey** (2 min):
95
+ - **Takeaway** (1 min):
96
+
97
+ #### 15-Minute Version
98
+ - **Opening** (2 min):
99
+ - **Problem Setup** (3 min):
100
+ - **Journey/Challenges** (5 min):
101
+ - **Insights/Solutions** (3 min):
102
+ - **Takeaways** (2 min):
103
+
104
+ #### 30-Minute Version
105
+ - **Introduction** (3 min):
106
+ - **Context/Background** (5 min):
107
+ - **Challenge Deep-Dive** (8 min):
108
+ - **Solution Journey** (8 min):
109
+ - **Lessons & Applications** (4 min):
110
+ - **Q&A Prep** (2 min):
111
+
112
+ ### Q&A Preparation
113
+ **Likely Questions**:
114
+ 1. Q:
115
+ A:
116
+
117
+ 2. Q:
118
+ A:
119
+
120
+ 3. Q:
121
+ A:
122
+
123
+ ## Quality Checklist
124
+ - [ ] Story is grounded in actual experience
125
+ - [ ] Avoids internal jargon
126
+ - [ ] Follows clear story arc (intent → friction → insight → outcome)
127
+ - [ ] Provides actionable lessons
128
+ - [ ] Sounds authentic, not sales-y
129
+ - [ ] Matches event format and audience
130
+ - [ ] Professional presentation materials ready