markform 0.1.21 → 0.1.22

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 (33) hide show
  1. package/README.md +32 -4
  2. package/dist/ai-sdk.d.mts +1 -1
  3. package/dist/ai-sdk.mjs +1 -1
  4. package/dist/{apply-CD-t7ovb.mjs → apply-C7mO7VkZ.mjs} +100 -74
  5. package/dist/apply-C7mO7VkZ.mjs.map +1 -0
  6. package/dist/bin.mjs +1 -1
  7. package/dist/{cli-ChdIy1a7.mjs → cli-C8F9yDsv.mjs} +17 -1213
  8. package/dist/cli-C8F9yDsv.mjs.map +1 -0
  9. package/dist/cli.mjs +1 -1
  10. package/dist/{coreTypes-BQrWf_Wt.d.mts → coreTypes-BlsJkU1w.d.mts} +1 -1
  11. package/dist/fillRecord-DTl5lnK0.d.mts +345 -0
  12. package/dist/fillRecordRenderer-CruJrLkj.mjs +1256 -0
  13. package/dist/fillRecordRenderer-CruJrLkj.mjs.map +1 -0
  14. package/dist/index.d.mts +5 -342
  15. package/dist/index.mjs +3 -3
  16. package/dist/render.d.mts +74 -0
  17. package/dist/render.mjs +4 -0
  18. package/dist/{session-ZgegwtkT.mjs → session-BCcltrLA.mjs} +1 -1
  19. package/dist/{session-ZgegwtkT.mjs.map → session-BCcltrLA.mjs.map} +1 -1
  20. package/dist/{session-BPuQ-ok0.mjs → session-VeSkVrck.mjs} +1 -1
  21. package/dist/{shared-DwdyWmvE.mjs → shared-CsdT2T7k.mjs} +1 -1
  22. package/dist/{shared-DwdyWmvE.mjs.map → shared-CsdT2T7k.mjs.map} +1 -1
  23. package/dist/{shared-BTR35aMz.mjs → shared-fb0nkzQi.mjs} +1 -1
  24. package/dist/{src-DOPe4tmu.mjs → src-CbRnGzMK.mjs} +16 -11
  25. package/dist/{src-DOPe4tmu.mjs.map → src-CbRnGzMK.mjs.map} +1 -1
  26. package/dist/urlFormat-lls7CsEP.mjs +71 -0
  27. package/dist/urlFormat-lls7CsEP.mjs.map +1 -0
  28. package/docs/markform-apis.md +53 -0
  29. package/examples/simple/simple-skipped-filled.report.md +8 -8
  30. package/examples/twitter-thread/twitter-thread.form.md +373 -0
  31. package/package.json +5 -1
  32. package/dist/apply-CD-t7ovb.mjs.map +0 -1
  33. package/dist/cli-ChdIy1a7.mjs.map +0 -1
@@ -0,0 +1,71 @@
1
+
2
+ //#region src/utils/urlFormat.ts
3
+ /**
4
+ * Create a friendly abbreviated display name for a URL.
5
+ * - Drops "www." prefix from domain
6
+ * - Adds first portion of path (up to maxPathChars) if present
7
+ * - Adds ellipsis (…) if path is truncated
8
+ *
9
+ * @param url - The URL to abbreviate
10
+ * @param maxPathChars - Maximum characters to include from the path (default: 12)
11
+ * @returns Friendly abbreviated URL (e.g., "example.com/docs/api…")
12
+ */
13
+ function friendlyUrlAbbrev(url, maxPathChars = 12) {
14
+ try {
15
+ const parsed = new URL(url);
16
+ let hostname = parsed.hostname;
17
+ if (hostname.startsWith("www.")) hostname = hostname.slice(4);
18
+ const path = parsed.pathname.slice(1);
19
+ if (!path) return hostname;
20
+ if (path.length <= maxPathChars) return `${hostname}/${path}`;
21
+ return `${hostname}/${path.slice(0, maxPathChars)}…`;
22
+ } catch {
23
+ let result = url;
24
+ result = result.replace(/^https?:\/\//, "");
25
+ result = result.replace(/^www\./, "");
26
+ const maxLen = 30;
27
+ if (result.length > maxLen) return result.slice(0, maxLen) + "…";
28
+ return result;
29
+ }
30
+ }
31
+ /**
32
+ * Format a URL as a markdown link with a friendly abbreviated display text.
33
+ * The full URL is preserved as the link target.
34
+ *
35
+ * @param url - The URL to format
36
+ * @returns Markdown link in format [friendly-abbrev](url)
37
+ */
38
+ function formatUrlAsMarkdownLink(url) {
39
+ return `[${friendlyUrlAbbrev(url)}](${url})`;
40
+ }
41
+ /**
42
+ * Format bare URLs in text as HTML links with abbreviated display text.
43
+ * Also handles markdown-style links [text](url) for consistency.
44
+ *
45
+ * Processing order:
46
+ * 1. Escape all HTML to prevent XSS
47
+ * 2. Convert markdown links [text](url) to <a> tags
48
+ * 3. Convert bare URLs (not already in links) to <a> tags with abbreviated display
49
+ *
50
+ * @param text - The raw text containing URLs (will be HTML-escaped)
51
+ * @param escapeHtml - Function to escape HTML entities
52
+ * @returns HTML-safe text with URLs converted to <a> tags
53
+ */
54
+ function formatBareUrlsAsHtmlLinks(text, escapeHtml) {
55
+ let result = escapeHtml(text);
56
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
57
+ const cleanUrl = url.replace(/&amp;/g, "&");
58
+ return `<a href="${escapeHtml(cleanUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(cleanUrl)}">${linkText}</a>`;
59
+ });
60
+ result = result.replace(/(?<!href="|data-url="|">)(?:https?:\/\/|www\.)[^\s<>"]+(?<![.,;:!?'")])/g, (url) => {
61
+ const cleanUrl = url.replace(/&amp;/g, "&");
62
+ const fullUrl = cleanUrl.startsWith("www.") ? `https://${cleanUrl}` : cleanUrl;
63
+ const display = friendlyUrlAbbrev(fullUrl);
64
+ return `<a href="${escapeHtml(fullUrl)}" target="_blank" class="url-link" data-url="${escapeHtml(fullUrl)}">${escapeHtml(display)}</a>`;
65
+ });
66
+ return result;
67
+ }
68
+
69
+ //#endregion
70
+ export { formatUrlAsMarkdownLink as n, friendlyUrlAbbrev as r, formatBareUrlsAsHtmlLinks as t };
71
+ //# sourceMappingURL=urlFormat-lls7CsEP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urlFormat-lls7CsEP.mjs","names":[],"sources":["../src/utils/urlFormat.ts"],"sourcesContent":["/**\n * URL formatting utilities for display and markdown output.\n */\n\n/**\n * Extract the domain (hostname) from a URL.\n * Returns the original string if parsing fails.\n *\n * @param url - The URL to extract the domain from\n * @returns The domain (e.g., \"example.com\") or the original string if invalid\n */\nexport function extractDomain(url: string): string {\n try {\n const parsed = new URL(url);\n return parsed.hostname;\n } catch {\n // If URL parsing fails, try to extract domain-like pattern\n const match = /^(?:https?:\\/\\/)?(?:www\\.)?([^/\\s]+)/i.exec(url);\n if (match?.[1]) {\n return match[1];\n }\n // Return original if we can't extract a domain\n return url;\n }\n}\n\n/**\n * Create a friendly abbreviated display name for a URL.\n * - Drops \"www.\" prefix from domain\n * - Adds first portion of path (up to maxPathChars) if present\n * - Adds ellipsis (…) if path is truncated\n *\n * @param url - The URL to abbreviate\n * @param maxPathChars - Maximum characters to include from the path (default: 12)\n * @returns Friendly abbreviated URL (e.g., \"example.com/docs/api…\")\n */\nexport function friendlyUrlAbbrev(url: string, maxPathChars = 12): string {\n try {\n const parsed = new URL(url);\n // Remove www. prefix from hostname\n let hostname = parsed.hostname;\n if (hostname.startsWith('www.')) {\n hostname = hostname.slice(4);\n }\n\n // Get path without leading slash, excluding query string and hash\n const path = parsed.pathname.slice(1);\n if (!path) {\n return hostname;\n }\n\n // Include path up to maxPathChars\n if (path.length <= maxPathChars) {\n return `${hostname}/${path}`;\n }\n\n // Truncate path and add ellipsis\n return `${hostname}/${path.slice(0, maxPathChars)}…`;\n } catch {\n // If URL parsing fails, try basic cleanup\n let result = url;\n // Remove protocol\n result = result.replace(/^https?:\\/\\//, '');\n // Remove www.\n result = result.replace(/^www\\./, '');\n // Truncate if too long\n const maxLen = 30;\n if (result.length > maxLen) {\n return result.slice(0, maxLen) + '…';\n }\n return result;\n }\n}\n\n/**\n * Format a URL as a markdown link with a friendly abbreviated display text.\n * The full URL is preserved as the link target.\n *\n * @param url - The URL to format\n * @returns Markdown link in format [friendly-abbrev](url)\n */\nexport function formatUrlAsMarkdownLink(url: string): string {\n const display = friendlyUrlAbbrev(url);\n return `[${display}](${url})`;\n}\n\n/**\n * Check if a string looks like a URL.\n *\n * @param str - The string to check\n * @returns true if the string appears to be a URL\n */\nexport function isUrl(str: string): boolean {\n // Check for common URL patterns\n if (str.startsWith('http://') || str.startsWith('https://')) {\n return true;\n }\n // Check for www. prefix\n if (str.startsWith('www.')) {\n return true;\n }\n return false;\n}\n\n/**\n * Format bare URLs in text as HTML links with abbreviated display text.\n * Also handles markdown-style links [text](url) for consistency.\n *\n * Processing order:\n * 1. Escape all HTML to prevent XSS\n * 2. Convert markdown links [text](url) to <a> tags\n * 3. Convert bare URLs (not already in links) to <a> tags with abbreviated display\n *\n * @param text - The raw text containing URLs (will be HTML-escaped)\n * @param escapeHtml - Function to escape HTML entities\n * @returns HTML-safe text with URLs converted to <a> tags\n */\nexport function formatBareUrlsAsHtmlLinks(text: string, escapeHtml: (s: string) => string): string {\n // SECURITY: Escape the entire text first to prevent XSS\n let result = escapeHtml(text);\n\n // Convert markdown links [text](url) to <a> tags\n // After escaping, we need to unescape &amp; back to & for URLs\n result = result.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (_match, linkText: string, url: string) => {\n const cleanUrl = url.replace(/&amp;/g, '&');\n return `<a href=\"${escapeHtml(cleanUrl)}\" target=\"_blank\" class=\"url-link\" data-url=\"${escapeHtml(cleanUrl)}\">${linkText}</a>`;\n });\n\n // Convert bare URLs to <a> tags with abbreviated display\n // Uses negative lookbehind to skip URLs that are:\n // - Inside href=\"\" or data-url=\"\" attributes\n // - Inside anchor tag content (preceded by \">)\n // Pattern matches http://, https://, www. URLs\n result = result.replace(\n /(?<!href=\"|data-url=\"|\">)(?:https?:\\/\\/|www\\.)[^\\s<>\"]+(?<![.,;:!?'\")])/g,\n (url: string) => {\n // Unescape &amp; back to & for the actual URL\n const cleanUrl = url.replace(/&amp;/g, '&');\n // Normalize www. URLs to have https://\n const fullUrl = cleanUrl.startsWith('www.') ? `https://${cleanUrl}` : cleanUrl;\n const display = friendlyUrlAbbrev(fullUrl);\n return `<a href=\"${escapeHtml(fullUrl)}\" target=\"_blank\" class=\"url-link\" data-url=\"${escapeHtml(fullUrl)}\">${escapeHtml(display)}</a>`;\n },\n );\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;AAoCA,SAAgB,kBAAkB,KAAa,eAAe,IAAY;AACxE,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,IAAI;EAE3B,IAAI,WAAW,OAAO;AACtB,MAAI,SAAS,WAAW,OAAO,CAC7B,YAAW,SAAS,MAAM,EAAE;EAI9B,MAAM,OAAO,OAAO,SAAS,MAAM,EAAE;AACrC,MAAI,CAAC,KACH,QAAO;AAIT,MAAI,KAAK,UAAU,aACjB,QAAO,GAAG,SAAS,GAAG;AAIxB,SAAO,GAAG,SAAS,GAAG,KAAK,MAAM,GAAG,aAAa,CAAC;SAC5C;EAEN,IAAI,SAAS;AAEb,WAAS,OAAO,QAAQ,gBAAgB,GAAG;AAE3C,WAAS,OAAO,QAAQ,UAAU,GAAG;EAErC,MAAM,SAAS;AACf,MAAI,OAAO,SAAS,OAClB,QAAO,OAAO,MAAM,GAAG,OAAO,GAAG;AAEnC,SAAO;;;;;;;;;;AAWX,SAAgB,wBAAwB,KAAqB;AAE3D,QAAO,IADS,kBAAkB,IAAI,CACnB,IAAI,IAAI;;;;;;;;;;;;;;;AAkC7B,SAAgB,0BAA0B,MAAc,YAA2C;CAEjG,IAAI,SAAS,WAAW,KAAK;AAI7B,UAAS,OAAO,QAAQ,6BAA6B,QAAQ,UAAkB,QAAgB;EAC7F,MAAM,WAAW,IAAI,QAAQ,UAAU,IAAI;AAC3C,SAAO,YAAY,WAAW,SAAS,CAAC,+CAA+C,WAAW,SAAS,CAAC,IAAI,SAAS;GACzH;AAOF,UAAS,OAAO,QACd,6EACC,QAAgB;EAEf,MAAM,WAAW,IAAI,QAAQ,UAAU,IAAI;EAE3C,MAAM,UAAU,SAAS,WAAW,OAAO,GAAG,WAAW,aAAa;EACtE,MAAM,UAAU,kBAAkB,QAAQ;AAC1C,SAAO,YAAY,WAAW,QAAQ,CAAC,+CAA+C,WAAW,QAAQ,CAAC,IAAI,WAAW,QAAQ,CAAC;GAErI;AAED,QAAO"}
@@ -546,6 +546,59 @@ interface InjectHeaderIdsOptions {
546
546
  }
547
547
  ```
548
548
 
549
+ ## Rendering API
550
+
551
+ Import from the render subpath for HTML rendering functions that produce the same output
552
+ as `markform serve`:
553
+
554
+ ```typescript
555
+ import {
556
+ renderViewContent,
557
+ renderSourceContent,
558
+ renderMarkdownContent,
559
+ renderYamlContent,
560
+ renderJsonContent,
561
+ renderFillRecordContent,
562
+ FILL_RECORD_STYLES,
563
+ FILL_RECORD_SCRIPTS,
564
+ escapeHtml,
565
+ formatDuration,
566
+ formatTokens,
567
+ } from 'markform/render';
568
+ ```
569
+
570
+ These functions produce HTML fragments (not full pages), so consumers can embed them in
571
+ their own page shell with their own layout, CSS reset, and surrounding UI.
572
+
573
+ ### Content Renderers
574
+
575
+ | Function | Input | Description |
576
+ | --- | --- | --- |
577
+ | `renderViewContent(form)` | `ParsedForm` | Render a form as a read-only HTML view |
578
+ | `renderSourceContent(content)` | `string` | Render Jinja-style form source with syntax highlighting |
579
+ | `renderMarkdownContent(content)` | `string` | Render markdown as HTML |
580
+ | `renderYamlContent(content)` | `string` | Render YAML with syntax highlighting |
581
+ | `renderJsonContent(content)` | `string` | Render JSON with syntax highlighting |
582
+ | `renderFillRecordContent(record)` | `FillRecord` | Render a fill record as an interactive dashboard |
583
+
584
+ ### CSS and JavaScript Constants
585
+
586
+ | Export | Description |
587
+ | --- | --- |
588
+ | `FILL_RECORD_STYLES` | `<style>` block with CSS for the fill record dashboard |
589
+ | `FILL_RECORD_SCRIPTS` | JavaScript providing `frShowTip()`, `frHideTip()`, `frCopyYaml()` for fill record interactivity |
590
+
591
+ Include `FILL_RECORD_STYLES` in your page `<head>` and `FILL_RECORD_SCRIPTS` in a
592
+ `<script>` tag when using `renderFillRecordContent()`.
593
+
594
+ ### Utility Functions
595
+
596
+ | Function | Description |
597
+ | --- | --- |
598
+ | `escapeHtml(str)` | Escape HTML special characters |
599
+ | `formatDuration(ms)` | Format milliseconds as human-readable duration (e.g., `"1m 5s"`) |
600
+ | `formatTokens(count)` | Format token counts with k suffix (e.g., `"1.5k"`) |
601
+
549
602
  ## Type Exports
550
603
 
551
604
  All Zod schemas and TypeScript types are exported from the main package:
@@ -19,7 +19,7 @@ test@example.com
19
19
 
20
20
  **Score:**
21
21
 
22
- _(empty)_
22
+ _(skipped: Not needed for this test)_
23
23
 
24
24
  ## List Fields
25
25
 
@@ -84,30 +84,30 @@ High
84
84
 
85
85
  **Team Members:**
86
86
 
87
- _(empty)_
87
+ _(skipped)_
88
88
 
89
89
  **Project Tasks:**
90
90
 
91
- _(empty)_
91
+ _(skipped)_
92
92
 
93
93
  ## Optional Fields
94
94
 
95
95
  **Notes:**
96
96
 
97
- _(empty)_
97
+ _(skipped: No notes required)_
98
98
 
99
99
  **Optional Number:**
100
100
 
101
- _(empty)_
101
+ _(skipped)_
102
102
 
103
103
  **Related URL:**
104
104
 
105
- _(empty)_
105
+ _(skipped: No related URL needed)_
106
106
 
107
107
  **Optional Date:**
108
108
 
109
- _(empty)_
109
+ _(skipped)_
110
110
 
111
111
  **Optional Year:**
112
112
 
113
- _(empty)_
113
+ _(skipped)_
@@ -0,0 +1,373 @@
1
+ ---
2
+ markform:
3
+ spec: MF/0.1
4
+ title: Content to Twitter Thread
5
+ description: Transform raw content into an engaging Twitter/X thread through structured analysis, prioritization, and iterative refinement.
6
+ run_mode: fill
7
+ roles:
8
+ - user
9
+ - agent
10
+ role_instructions:
11
+ user: |
12
+ Provide your raw content and any context about your audience and goals.
13
+ The content can be a transcript, blog draft, notes, or any text.
14
+ Include as much detail as possible: specific examples, names, links, code.
15
+ agent: |
16
+ Transform the input into a compelling Twitter thread. Work through each stage in order—do not skip stages.
17
+
18
+ VOICE AND STYLE (CRITICAL):
19
+ - Write like a smart person talking, not a press release
20
+ - Be informal: use contractions, conversational phrasing
21
+ - Be opinionated: take strong positions, not wishy-washy hedging
22
+ - Be specific: name people, cite sources, link to things
23
+ - Be novel: each tweet should make the reader pause and think
24
+ - AVOID: generic "sounds like perfect English" corporate speak
25
+ - AVOID: vague claims like "improves quality" without showing HOW
26
+
27
+ Good example: "LLMs are, first and foremost, fuzzy subgraph matching machines"
28
+ Bad example: "LLMs have limitations in certain reasoning tasks"
29
+
30
+ Good example: "Like Katalin Karikó, for instance" (names a specific person)
31
+ Bad example: "Like many successful researchers" (generic)
32
+
33
+ STAGE 1 - CLEANUP: Edit raw content. PRESERVE specific names, examples, links, code.
34
+
35
+ STAGE 2 - EXTRACT INSIGHTS: Find CONCRETE, NOVEL insights worth sharing.
36
+ Look for:
37
+ - Specific examples with names/links (people, repos, books, episodes)
38
+ - Technical details that are precise and memorable
39
+ - Personal observations ("As a researcher, I find...")
40
+ - Contrarian or surprising claims
41
+ - Things that make you go "huh, I never thought of it that way"
42
+ AVOID: Generic claims that could be in any article on the topic
43
+
44
+ STAGE 3 - PRIORITIZE: Rank by NOVELTY and specificity.
45
+ The hook should be surprising or contrarian, not a generic claim.
46
+ Include tweets that link to specific things (repos, docs, people, books).
47
+
48
+ STAGE 4 - STRUCTURE: Plan the thread. Longer is fine (15-25 tweets) if every
49
+ tweet has real substance. Include:
50
+ - Specific examples with links
51
+ - Technical details that are precise
52
+ - Personal takes and opinions
53
+ - At least 2-3 tweets that reference specific things to click/explore
54
+
55
+ STAGE 5 - DRAFT: Write informally. Sound like a person, not a brand.
56
+ - Use contractions (I've, it's, that's)
57
+ - Use parentheticals for asides (if you're a researcher, you'll have noticed)
58
+ - Be direct and opinionated
59
+ - Include specific names, links, code when relevant
60
+
61
+ STAGE 6 - REVIEW: Check each tweet:
62
+ - Does it sound like a person talking? (not corporate)
63
+ - Is it specific? (names, examples, links)
64
+ - Is it novel? (makes you think, not just nod)
65
+ - Would someone quote-tweet this specific insight?
66
+
67
+ STAGE 7 - FINAL: Produce polished output. Every tweet should be quotable.
68
+
69
+ Key principles:
70
+ - Generic = worthless. Specific = valuable.
71
+ - Sound like yourself, not like marketing copy
72
+ - Each tweet should teach something or make someone think
73
+ - Include things to click on (links, repos, names to search)
74
+ harness_config:
75
+ max_issues_per_turn: 5
76
+ max_patches_per_turn: 15
77
+ ---
78
+ <!-- form id="twitter_thread" title="Content to Twitter Thread" -->
79
+
80
+ <!-- description ref="twitter_thread" -->
81
+ A rigorous content transformation workflow: raw text → insights → prioritization → structure → drafts → review → final thread. Each stage enforces quality through structured tables and validation.
82
+ <!-- /description -->
83
+
84
+ <!-- group id="input" title="Input: Raw Content" -->
85
+
86
+ ## Source Content
87
+
88
+ <!-- field kind="string" id="raw_content" label="Raw Content" role="user" required=true minLength=200 --><!-- /field -->
89
+
90
+ <!-- instructions ref="raw_content" -->
91
+ Paste your raw content. This can be:
92
+ - A rough transcript (talk, podcast, voice memo)
93
+ - A blog post or article draft
94
+ - Meeting notes, brainstorm, or research notes
95
+ - Any text you want to transform
96
+
97
+ Aim for at least a few paragraphs. More content = richer thread options.
98
+ <!-- /instructions -->
99
+
100
+ <!-- field kind="string" id="target_audience" label="Target Audience" role="user" maxLength=200 --><!-- /field -->
101
+
102
+ <!-- instructions ref="target_audience" -->
103
+ Who is this thread for? Examples:
104
+ - "Startup founders building AI products"
105
+ - "Developers learning system design"
106
+ - "Product managers in B2B SaaS"
107
+
108
+ Helps tailor tone, examples, and assumed knowledge level.
109
+ <!-- /instructions -->
110
+
111
+ <!-- field kind="string" id="thread_goal" label="Thread Goal" role="user" maxLength=300 --><!-- /field -->
112
+
113
+ <!-- instructions ref="thread_goal" -->
114
+ What should readers take away? Examples:
115
+ - "Understand why structured forms improve AI agent reliability"
116
+ - "Learn the 5 key principles of good API design"
117
+ - "See why this approach to X is underrated"
118
+ <!-- /instructions -->
119
+
120
+ <!-- field kind="number" id="target_length" label="Target Thread Length" role="user" min=5 max=20 integer=true --><!-- /field -->
121
+
122
+ <!-- instructions ref="target_length" -->
123
+ Desired number of tweets (5-20). Typical threads:
124
+ - 5-7 tweets: Quick insight or single concept
125
+ - 8-12 tweets: Moderate depth, multiple points
126
+ - 12-20 tweets: Deep dive, comprehensive coverage
127
+ <!-- /instructions -->
128
+
129
+ <!-- /group -->
130
+
131
+ <!-- group id="cleanup" title="Stage 1: Content Cleanup" -->
132
+
133
+ ## Cleaned Content
134
+
135
+ <!-- field kind="string" id="cleaned_content" label="Cleaned Content" role="agent" required=true minLength=100 --><!-- /field -->
136
+
137
+ <!-- instructions ref="cleaned_content" -->
138
+ Edit the raw content into clean, readable prose:
139
+ - Fix transcription errors, typos, and grammatical issues
140
+ - Remove filler words (um, uh, like, you know, basically)
141
+ - Improve sentence flow and clarity
142
+ - Preserve the author's voice and distinctive phrases
143
+ - Don't add new ideas—just clean what's there
144
+ <!-- /instructions -->
145
+
146
+ <!-- /group -->
147
+
148
+ <!-- group id="insights" title="Stage 2: Extract Insights" -->
149
+
150
+ ## Key Insights
151
+
152
+ Extract every insight, claim, or idea worth sharing.
153
+
154
+ <!-- field kind="table" id="insights_table" label="Insights" role="agent" required=true
155
+ columnIds=["insight", "why_matters", "type"]
156
+ columnLabels=["Insight/Claim", "Why It Matters", "Type"]
157
+ columnTypes=["string", "string", "string"]
158
+ minRows=5 maxRows=20 -->
159
+
160
+ | Insight/Claim | Why It Matters | Type |
161
+ |---------------|----------------|------|
162
+
163
+ <!-- /field -->
164
+
165
+ <!-- instructions ref="insights_table" -->
166
+ For each insight from the cleaned content:
167
+ - **Insight/Claim**: The core idea in one sentence
168
+ - **Why It Matters**: Why should readers care? What's the payoff?
169
+ - **Type**: categorize as one of:
170
+ - `thesis` - central argument
171
+ - `supporting` - backs up the thesis
172
+ - `example` - concrete illustration
173
+ - `context` - background/setup
174
+ - `contrarian` - challenges assumptions
175
+ - `actionable` - something readers can do
176
+
177
+ Be thorough—extract MORE than you'll use. You'll prioritize in the next stage.
178
+ <!-- /instructions -->
179
+
180
+ <!-- /group -->
181
+
182
+ <!-- group id="prioritize" title="Stage 3: Prioritize & Rank" -->
183
+
184
+ ## Prioritization
185
+
186
+ Not every insight makes the thread. Rank by impact and assign roles.
187
+
188
+ <!-- field kind="table" id="priority_table" label="Ranked Insights" role="agent" required=true
189
+ columnIds=["rank", "insight_summary", "role", "include"]
190
+ columnLabels=["Rank", "Insight (summary)", "Thread Role", "Include?"]
191
+ columnTypes=["number", "string", "string", "string"]
192
+ minRows=5 maxRows=15 -->
193
+
194
+ | Rank | Insight (summary) | Thread Role | Include? |
195
+ |------|-------------------|-------------|----------|
196
+
197
+ <!-- /field -->
198
+
199
+ <!-- instructions ref="priority_table" -->
200
+ Take insights from Stage 2 and prioritize:
201
+ - **Rank**: 1 = highest impact, most compelling
202
+ - **Insight**: Brief summary (reference the insight)
203
+ - **Thread Role**: Where it fits in the thread:
204
+ - `hook` - attention-grabbing opener (need exactly 1)
205
+ - `context` - setup/background
206
+ - `main_point` - core argument support
207
+ - `example` - concrete illustration
208
+ - `pivot` - transition or "but here's the thing"
209
+ - `summary` - ties it together
210
+ - `cta` - call to action (need exactly 1)
211
+ - **Include?**: `yes` or `no` (cut ruthlessly—not everything fits)
212
+
213
+ Total "yes" should roughly match target thread length.
214
+ <!-- /instructions -->
215
+
216
+ <!-- field kind="string" id="hook_strategy" label="Hook Strategy" role="agent" required=true maxLength=400 --><!-- /field -->
217
+
218
+ <!-- instructions ref="hook_strategy" -->
219
+ What's the hook strategy? Choose one:
220
+ - **Contrarian**: "Most people think X, but actually Y"
221
+ - **Promise**: "Here's how to do X in N steps"
222
+ - **Story**: "Last week I learned something that changed how I think about X"
223
+ - **Bold claim**: "X is the most underrated skill in Y"
224
+ - **Question**: "Why do most X fail at Y?"
225
+
226
+ Write out your specific hook angle for this thread.
227
+ <!-- /instructions -->
228
+
229
+ <!-- /group -->
230
+
231
+ <!-- group id="structure" title="Stage 4: Thread Structure" -->
232
+
233
+ ## Thread Plan
234
+
235
+ Map the prioritized insights into a tweet-by-tweet structure.
236
+
237
+ <!-- field kind="table" id="structure_table" label="Thread Structure" role="agent" required=true
238
+ columnIds=["tweet_num", "role", "content_plan", "source_insight"]
239
+ columnLabels=["#", "Role", "Content Plan", "From Insight"]
240
+ columnTypes=["number", "string", "string", "string"]
241
+ minRows=5 maxRows=20 -->
242
+
243
+ | # | Role | Content Plan | From Insight |
244
+ |---|------|--------------|--------------|
245
+
246
+ <!-- /field -->
247
+
248
+ <!-- instructions ref="structure_table" -->
249
+ Plan each tweet position:
250
+ - **#**: Tweet number (1, 2, 3...)
251
+ - **Role**: hook, context, main_point, example, pivot, summary, cta
252
+ - **Content Plan**: What this tweet will say (not the final text, just the plan)
253
+ - **From Insight**: Which insight from the priority table this uses (or "new" if synthesized)
254
+
255
+ This is your blueprint. Drafting should follow this plan.
256
+ <!-- /instructions -->
257
+
258
+ <!-- /group -->
259
+
260
+ <!-- group id="drafts" title="Stage 5: Draft Tweets" -->
261
+
262
+ ## Tweet Drafts
263
+
264
+ Write each tweet following the structure plan.
265
+
266
+ <!-- field kind="table" id="drafts_table" label="Tweet Drafts" role="agent" required=true
267
+ columnIds=["tweet_num", "draft_content", "char_count", "issues"]
268
+ columnLabels=["#", "Draft Content", "Chars", "Issues"]
269
+ columnTypes=["number", "string", "number", "string"]
270
+ minRows=5 maxRows=20 -->
271
+
272
+ | # | Draft Content | Chars | Issues |
273
+ |---|---------------|-------|--------|
274
+
275
+ <!-- /field -->
276
+
277
+ <!-- instructions ref="drafts_table" -->
278
+ Write each tweet:
279
+ - **#**: Tweet number, must include "N/" prefix in the content
280
+ - **Draft Content**: The tweet text (must be ≤280 characters)
281
+ - **Chars**: Character count (be accurate!)
282
+ - **Issues**: Note any problems: "too long", "weak hook", "unclear", "missing link to previous"
283
+
284
+ Rules:
285
+ - Hard limit: 280 characters per tweet
286
+ - Include thread number prefix (1/, 2/, etc.)
287
+ - Each tweet should make sense if quoted alone
288
+ - Line breaks within tweets are fine for readability
289
+ <!-- /instructions -->
290
+
291
+ <!-- /group -->
292
+
293
+ <!-- group id="review" title="Stage 6: Review & Refine" -->
294
+
295
+ ## Review Checklist
296
+
297
+ Verify each tweet against quality criteria.
298
+
299
+ <!-- field kind="table" id="review_table" label="Tweet Review" role="agent" required=true
300
+ columnIds=["tweet_num", "under_280", "clear", "flows", "standalone", "revision_needed"]
301
+ columnLabels=["#", "≤280?", "Clear?", "Flows?", "Standalone?", "Revision"]
302
+ columnTypes=["number", "string", "string", "string", "string", "string"]
303
+ minRows=5 maxRows=20 -->
304
+
305
+ | # | ≤280? | Clear? | Flows? | Standalone? | Revision |
306
+ |---|-------|--------|--------|-------------|----------|
307
+
308
+ <!-- /field -->
309
+
310
+ <!-- instructions ref="review_table" -->
311
+ Review each tweet:
312
+ - **≤280?**: Is it under 280 characters? (yes/no)
313
+ - **Clear?**: Is the point immediately clear? (yes/no)
314
+ - **Flows?**: Does it connect naturally from the previous tweet? (yes/no/na for #1)
315
+ - **Standalone?**: Would it make sense if someone only saw this tweet? (yes/no)
316
+ - **Revision**: What needs fixing? Leave blank if good, otherwise note the issue.
317
+
318
+ If any issues exist, address them before finalizing.
319
+ <!-- /instructions -->
320
+
321
+ <!-- field kind="string" id="revision_notes" label="Revision Notes" role="agent" maxLength=500 --><!-- /field -->
322
+
323
+ <!-- instructions ref="revision_notes" -->
324
+ If any tweets needed revision based on the review, note what you changed.
325
+ Leave blank if no revisions were needed.
326
+ <!-- /instructions -->
327
+
328
+ <!-- /group -->
329
+
330
+ <!-- group id="final" title="Stage 7: Final Thread" -->
331
+
332
+ ## Final Output
333
+
334
+ The polished, copy-paste ready thread.
335
+
336
+ <!-- field kind="string" id="final_thread" label="Final Thread" role="agent" required=true --><!-- /field -->
337
+
338
+ <!-- instructions ref="final_thread" -->
339
+ Produce the final thread in copy-paste format:
340
+
341
+ 1/ [First tweet]
342
+
343
+ 2/ [Second tweet]
344
+
345
+ 3/ [Third tweet]
346
+
347
+ ...
348
+
349
+ (Double line break between each tweet for easy copying)
350
+ <!-- /instructions -->
351
+
352
+ <!-- field kind="number" id="total_tweets" label="Total Tweet Count" role="agent" required=true min=5 max=20 integer=true --><!-- /field -->
353
+
354
+ <!-- field kind="checkboxes" id="final_checklist" label="Final Quality Checklist" role="agent" checkboxMode="simple" required=true -->
355
+
356
+ - [ ] Hook grabs attention and is specific to the content <!-- #hook_quality -->
357
+ - [ ] Every tweet is under 280 characters <!-- #char_limit -->
358
+ - [ ] Thread has clear narrative arc (setup → points → conclusion) <!-- #narrative_arc -->
359
+ - [ ] Each tweet flows naturally from the previous one <!-- #flow_quality -->
360
+ - [ ] Each tweet makes sense if read in isolation <!-- #standalone -->
361
+ - [ ] Ends with clear CTA or memorable summary <!-- #ending_quality -->
362
+ - [ ] No filler tweets—every tweet adds value <!-- #no_filler -->
363
+
364
+ <!-- /field -->
365
+
366
+ <!-- instructions ref="final_checklist" -->
367
+ Verify ALL quality criteria are met before marking complete.
368
+ Do not check a box unless you've verified it's true.
369
+ <!-- /instructions -->
370
+
371
+ <!-- /group -->
372
+
373
+ <!-- /form -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markform",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Markdown forms for token-friendly workflows",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "Joshua Levy",
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/ai-sdk.d.mts",
41
41
  "default": "./dist/ai-sdk.mjs"
42
42
  },
43
+ "./render": {
44
+ "types": "./dist/render.d.mts",
45
+ "default": "./dist/render.mjs"
46
+ },
43
47
  "./package.json": "./package.json"
44
48
  },
45
49
  "bin": {