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.
- package/README.md +32 -4
- package/dist/ai-sdk.d.mts +1 -1
- package/dist/ai-sdk.mjs +1 -1
- package/dist/{apply-CD-t7ovb.mjs → apply-C7mO7VkZ.mjs} +100 -74
- package/dist/apply-C7mO7VkZ.mjs.map +1 -0
- package/dist/bin.mjs +1 -1
- package/dist/{cli-ChdIy1a7.mjs → cli-C8F9yDsv.mjs} +17 -1213
- package/dist/cli-C8F9yDsv.mjs.map +1 -0
- package/dist/cli.mjs +1 -1
- package/dist/{coreTypes-BQrWf_Wt.d.mts → coreTypes-BlsJkU1w.d.mts} +1 -1
- package/dist/fillRecord-DTl5lnK0.d.mts +345 -0
- package/dist/fillRecordRenderer-CruJrLkj.mjs +1256 -0
- package/dist/fillRecordRenderer-CruJrLkj.mjs.map +1 -0
- package/dist/index.d.mts +5 -342
- package/dist/index.mjs +3 -3
- package/dist/render.d.mts +74 -0
- package/dist/render.mjs +4 -0
- package/dist/{session-ZgegwtkT.mjs → session-BCcltrLA.mjs} +1 -1
- package/dist/{session-ZgegwtkT.mjs.map → session-BCcltrLA.mjs.map} +1 -1
- package/dist/{session-BPuQ-ok0.mjs → session-VeSkVrck.mjs} +1 -1
- package/dist/{shared-DwdyWmvE.mjs → shared-CsdT2T7k.mjs} +1 -1
- package/dist/{shared-DwdyWmvE.mjs.map → shared-CsdT2T7k.mjs.map} +1 -1
- package/dist/{shared-BTR35aMz.mjs → shared-fb0nkzQi.mjs} +1 -1
- package/dist/{src-DOPe4tmu.mjs → src-CbRnGzMK.mjs} +16 -11
- package/dist/{src-DOPe4tmu.mjs.map → src-CbRnGzMK.mjs.map} +1 -1
- package/dist/urlFormat-lls7CsEP.mjs +71 -0
- package/dist/urlFormat-lls7CsEP.mjs.map +1 -0
- package/docs/markform-apis.md +53 -0
- package/examples/simple/simple-skipped-filled.report.md +8 -8
- package/examples/twitter-thread/twitter-thread.form.md +373 -0
- package/package.json +5 -1
- package/dist/apply-CD-t7ovb.mjs.map +0 -1
- 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(/&/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(/&/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 & back to & for URLs\n result = result.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (_match, linkText: string, url: string) => {\n const cleanUrl = url.replace(/&/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 & back to & for the actual URL\n const cleanUrl = url.replace(/&/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"}
|
package/docs/markform-apis.md
CHANGED
|
@@ -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
|
-
_(
|
|
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
|
-
_(
|
|
87
|
+
_(skipped)_
|
|
88
88
|
|
|
89
89
|
**Project Tasks:**
|
|
90
90
|
|
|
91
|
-
_(
|
|
91
|
+
_(skipped)_
|
|
92
92
|
|
|
93
93
|
## Optional Fields
|
|
94
94
|
|
|
95
95
|
**Notes:**
|
|
96
96
|
|
|
97
|
-
_(
|
|
97
|
+
_(skipped: No notes required)_
|
|
98
98
|
|
|
99
99
|
**Optional Number:**
|
|
100
100
|
|
|
101
|
-
_(
|
|
101
|
+
_(skipped)_
|
|
102
102
|
|
|
103
103
|
**Related URL:**
|
|
104
104
|
|
|
105
|
-
_(
|
|
105
|
+
_(skipped: No related URL needed)_
|
|
106
106
|
|
|
107
107
|
**Optional Date:**
|
|
108
108
|
|
|
109
|
-
_(
|
|
109
|
+
_(skipped)_
|
|
110
110
|
|
|
111
111
|
**Optional Year:**
|
|
112
112
|
|
|
113
|
-
_(
|
|
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.
|
|
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": {
|