@theseus.run/jsx-md 0.1.2 → 0.2.0

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 CHANGED
@@ -1,81 +1,14 @@
1
1
  # `@theseus.run/jsx-md`
2
2
 
3
- Built to manage agent instructions across multiple harnesses [Copilot](https://github.com/features/copilot), [OpenCode](https://opencode.ai), and others. The content wasn't the problem. The management was.
3
+ JSX runtime that outputs Markdown stringstyped props, Context API, XML intrinsics, zero runtime dependencies.
4
4
 
5
- Agent instruction files grow. They develop conditional sections — capabilities that only apply to one harness, traits you're toggling between first-person and third-person to test what produces better behavior. A shared fragment between two agents becomes a copy-paste. A conditional becomes a ternary inside a template literal:
5
+ [Why this exists](https://romanonthego.dev/blog/jsx-that-outputs-markdown)
6
6
 
7
- ```typescript
8
- const instructions = `
9
- You are a code reviewer.
7
+ ## Prior art
10
8
 
11
- ${harness === 'opencode' ? '<!-- use task() for subtasks -->' : ''}
9
+ [dbartholomae/jsx-md](https://github.com/dbartholomae/jsx-md) (2019, inactive) and [eyelly-wu/jsx-to-md](https://github.com/eyelly-wu/jsx-to-md) are built for documentation generation READMEs, changelogs. They work well for that. `dbartholomae/jsx-md` predates `jsxImportSource` and uses file-level pragma comments; its `render()` returns a Promise.
12
10
 
13
- ## Traits
14
- ${firstPerson
15
- ? '- I always verify output before claiming done.'
16
- : '- The agent must verify output before claiming done.'}
17
- `
18
- ```
19
-
20
- No syntax highlighting for the Markdown inside the string. No types on the structure. A variant is a new file. Refactoring is grep-and-pray.
21
-
22
- Nested lists make it worse. Markdown requires exact indentation — two spaces per level. Template strings force you to hardcode that spacing:
23
-
24
- ```typescript
25
- const instructions = `
26
- ## Rules
27
-
28
- - Outer rule
29
- - Nested rule: two hardcoded spaces
30
- - Deeper: four hardcoded spaces
31
- ${condition ? ' - Conditional nested item' : ''}
32
- `
33
- ```
34
-
35
- A false conditional leaves a blank bullet. The indentation is load-bearing and invisible. `DepthContext` in `@theseus.run/jsx-md` tracks nesting depth automatically — you write `<Ul>` inside `<Li>` and the renderer handles the spaces.
36
-
37
- This isn't an edge case. [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent) — 39.8k stars, serious harness work — stores instructions the same way:
38
-
39
- ```typescript
40
- export const PROMETHEUS_HIGH_ACCURACY_MODE = `# PHASE 3: PLAN GENERATION
41
- ## High Accuracy Mode - MANDATORY LOOP
42
- \`\`\`typescript
43
- while (true) {
44
- const result = task(subagent_type="momus", ...)
45
- if (result.verdict === "OKAY") break
46
- }
47
- \`\`\`
48
- ...` // 62 more lines of escaped template string
49
- ```
50
-
51
- The escaped backticks are the tell — not a bad solution, it's the only solution the format offers. No composability, no reuse, no tooling support.
52
-
53
- The web dev world solved "structured text with conditionals and composable fragments" ten years ago. JSX is a transform spec, not a React dependency — `jsxImportSource` lets you point it at any runtime. `@theseus.run/jsx-md` is a runtime that outputs Markdown:
54
-
55
- ```tsx
56
- const ReviewerInstructions = ({ harness, firstPerson }: Props) => (
57
- <>
58
- <P>You are a code reviewer.</P>
59
- {harness === 'opencode' && (
60
- <HtmlComment>use task() for subtasks</HtmlComment>
61
- )}
62
- <H2>Traits</H2>
63
- <Ul>
64
- <Li>
65
- {firstPerson
66
- ? 'I always verify output before claiming done.'
67
- : 'The agent must verify output before claiming done.'}
68
- </Li>
69
- </Ul>
70
- </>
71
- )
72
-
73
- render(<ReviewerInstructions harness="opencode" firstPerson={true} />)
74
- ```
75
-
76
- No escaped backticks. Syntax highlighting in your editor. Conditionals are JSX expressions. Variants are props. Shared fragments are components.
77
-
78
- `render()` returns a plain string — no virtual DOM, no hydration, no React runtime anywhere in the chain.
11
+ `@theseus.run/jsx-md` targets agent instructions assembled at call time: `render()` is synchronous, Context API ships with it, and any lowercase tag is an XML intrinsic.
79
12
 
80
13
  ---
81
14
 
@@ -83,9 +16,11 @@ No escaped backticks. Syntax highlighting in your editor. Conditionals are JSX e
83
16
 
84
17
  ```bash
85
18
  bun add @theseus.run/jsx-md
19
+ # npm install @theseus.run/jsx-md
20
+ # pnpm add @theseus.run/jsx-md
86
21
  ```
87
22
 
88
- Then in `tsconfig.json`:
23
+ `tsconfig.json`:
89
24
 
90
25
  ```json
91
26
  {
@@ -96,84 +31,71 @@ Then in `tsconfig.json`:
96
31
  }
97
32
  ```
98
33
 
99
- ---
100
-
101
- ## What you're getting
102
-
103
- **Zero runtime dependencies.** The package ships TypeScript source with no third-party imports. Nothing gets added to your bundle.
104
-
105
- **Bun-first.** Ships TypeScript source directly, no compiled output. Works transparently in Bun: install, add two tsconfig lines, write TSX. The trade-off is real: vanilla Node.js without a bundler won't run `.ts` files from `node_modules`. If your stack is Vite, tsup, or esbuild, those handle it. If you're running bare Node, v0.1.0 doesn't have a solution for you yet.
106
-
107
- **JSX without React.** When you write `<H2>Title</H2>`, the transform calls `jsx(H2, { children: "Title" })`. `H2` is a plain function — takes props, returns a string. No virtual DOM, no reconciler, no fiber, no hooks. `render()` walks the VNode tree synchronously and concatenates. The output is deterministic: same input, same string, every time. You can test it with `expect(render(<MyPrompt />)).toMatchSnapshot()`.
108
-
109
- **TypeScript-first.** All components and their props are typed. `render()` accepts `VNode`, returns `string`. Wrong usage is a compile error, not a runtime surprise.
110
-
111
- **String children are verbatim.** `render()` passes raw string values through without escaping. `<P>{"<b>text</b>"}</P>` outputs `<b>text</b>` — no transformation. For LLM prompts this is the right default; Markdown renderers handle the rest.
112
-
113
- **XML-structured prompts, zero config.** Any lowercase JSX tag renders as an XML block — `<context>`, `<instructions>`, `<example index={1}>`. Anthropic's prompt engineering guide recommends this structure for Claude agents. Attributes are typed and serialized automatically; empty tags self-close. No imports, no registration — it's built into the JSX intrinsics catch-all.
34
+ Ships TypeScript source and compiled ESM output with sourcemaps. Bun resolves the TypeScript source directly. Node.js (≥18) and bundlers (Vite, tsup, esbuild) use the compiled output — no configuration needed, the `exports` map handles it transparently.
114
35
 
115
36
  ---
116
37
 
117
38
  ## Usage
118
39
 
119
40
  ```tsx
120
- // system-prompt.tsx
121
41
  import { render, H2, P, Ul, Li, Bold, Code } from "@theseus.run/jsx-md";
122
42
 
123
- const prompt = render(
43
+ const ReviewerPrompt = ({ repo }: { repo: string }) => (
124
44
  <>
125
45
  <H2>Role</H2>
126
- <P>
127
- You are a precise code reviewer. Your job is to find bugs, not suggest
128
- style changes.
129
- </P>
46
+ <P>You are a precise code reviewer. Find bugs, not style issues.</P>
130
47
 
131
48
  <H2>Rules</H2>
132
49
  <Ul>
133
- <Li>
134
- Flag <Bold>P0</Bold> issues immediately — do not bury them.
135
- </Li>
136
- <Li>
137
- Use <Code>inline code</Code> when referencing identifiers.
138
- </Li>
50
+ <Li>Flag <Bold>P0</Bold> issues first — do not bury them.</Li>
139
51
  <Li>One finding per comment. No compound observations.</Li>
52
+ <Li>Use <Code>inline code</Code> when referencing identifiers.</Li>
140
53
  </Ul>
141
-
142
- <H2>Output format</H2>
143
- <P>
144
- Respond with a structured list. Each item: severity, location, finding.
145
- </P>
146
54
  </>
147
55
  );
148
56
 
149
- console.log(prompt);
57
+ const prompt = render(<ReviewerPrompt repo="cockpit" />);
58
+ // "## Role\n\nYou are a precise code reviewer..."
150
59
  ```
151
60
 
152
- Output:
61
+ `render()` returns a plain string. No virtual DOM, no React runtime. Same input, same string, every time.
62
+
63
+ ---
153
64
 
154
- ```markdown
155
- ## Role
65
+ ## Context API
156
66
 
157
- You are a precise code reviewer. Your job is to find bugs, not suggest style changes.
67
+ Avoids prop-drilling through shared fragment trees. Same shape as React `createContext`, `useContext`, `Context.Provider` synchronous, no rules-of-hooks.
158
68
 
159
- ## Rules
69
+ ```tsx
70
+ import { render, createContext, useContext, Ul, Li, Code } from "@theseus.run/jsx-md";
160
71
 
161
- - Flag **P0** issues immediately — do not bury them.
162
- - Use `inline code` when referencing identifiers.
163
- - One finding per comment. No compound observations.
72
+ const HarnessCtx = createContext<'opencode' | 'copilot'>('copilot');
164
73
 
165
- ## Output format
74
+ const StepsSection = () => {
75
+ const harness = useContext(HarnessCtx);
76
+ return (
77
+ <Ul>
78
+ <Li>Always verify output before claiming done.</Li>
79
+ {harness === 'opencode' && <Li>Use <Code>task()</Code> for multi-step subtasks.</Li>}
80
+ </Ul>
81
+ );
82
+ };
166
83
 
167
- Respond with a structured list. Each item: severity, location, finding.
84
+ // Wire it once at the root no prop threading:
85
+ const prompt = render(
86
+ <HarnessCtx.Provider value="opencode">
87
+ <StepsSection />
88
+ </HarnessCtx.Provider>
89
+ );
168
90
  ```
169
91
 
170
- ---
92
+ `useContext` returns the default when called outside a Provider. Providers nest — innermost wins.
171
93
 
172
- ## Structured agent instructions
94
+ ---
173
95
 
174
- Anthropic's prompt engineering guide recommends XML tags to structure Claude prompts — wrapping each content type in its own tag reduces misinterpretation. `<instructions>`, `<context>`, `<examples>`, `<document index="n">` are the documented patterns.
96
+ ## XML intrinsics
175
97
 
176
- Any lowercase JSX tag is an XML intrinsic in `@theseus.run/jsx-md`. No imports, no registration:
98
+ Any lowercase JSX tag renders as an XML block. No imports, no registration:
177
99
 
178
100
  ```tsx
179
101
  const ReviewerPrompt = ({ repo, examples }: Props) => (
@@ -185,13 +107,6 @@ const ReviewerPrompt = ({ repo, examples }: Props) => (
185
107
  <instructions>
186
108
  <H2>Role</H2>
187
109
  <P>You are a precise code reviewer. Find bugs, not style issues.</P>
188
-
189
- <H2>Rules</H2>
190
- <Ul>
191
- <Li>Flag <Bold>P0</Bold> issues first — do not bury them.</Li>
192
- <Li>One finding per comment. No compound observations.</Li>
193
- <Li>Use <Code>inline code</Code> when referencing identifiers.</Li>
194
- </Ul>
195
110
  </instructions>
196
111
 
197
112
  {examples.length > 0 && (
@@ -204,120 +119,73 @@ const ReviewerPrompt = ({ repo, examples }: Props) => (
204
119
  </examples>
205
120
  )}
206
121
  </>
207
- )
208
- ```
209
-
210
- Output:
211
-
212
- ```
213
- <context>
214
- Repository: cockpit. Language: TypeScript. Package manager: bun.
215
- </context>
216
-
217
- <instructions>
218
- ## Role
219
-
220
- You are a precise code reviewer. Find bugs, not style issues.
221
-
222
- ## Rules
223
-
224
- - Flag **P0** issues first — do not bury them.
225
- - One finding per comment. No compound observations.
226
- - Use `inline code` when referencing identifiers.
227
- </instructions>
228
-
229
- <examples>
230
- <example index="1">
231
- ... your example content ...
232
- </example>
233
- </examples>
122
+ );
234
123
  ```
235
124
 
236
- Attributes are typed (`index={1}` `index="1"`), boolean `true` attrs render bare, `false`/`null`/`undefined` are omitted. Empty tags self-close: `<tag />`.
125
+ Attributes are typed `index={1}` serializes to `index="1"`. Boolean `true` renders bare, `false`/`null`/`undefined` are omitted. Empty tags self-close.
237
126
 
238
127
  ---
239
128
 
240
129
  ## Primitives
241
130
 
242
- | Component | Markdown output |
243
- |-----------|----------------|
131
+ | Component | Output |
132
+ |---|---|
244
133
  | `H1`–`H6` | `#`–`######` headings |
245
134
  | `P` | Paragraph (blank line separated) |
246
135
  | `Hr` | `---` horizontal rule |
247
- | `Codeblock` | Fenced code block with optional `lang` prop |
136
+ | `Codeblock` | Fenced code block (`lang` prop optional) |
248
137
  | `Blockquote` | `>` blockquote |
249
138
  | `Ul` | Unordered list |
250
- | `Ol` | Ordered list |
251
- | `Li` | List item (supports nesting via `Ul`/`Ol` inside `Li`) |
139
+ | `Ol` | Ordered list (auto-numbered, nesting supported) |
140
+ | `Li` | List item (supports nested `Ul` or `Ol` inside `Li`) |
252
141
  | `TaskList` | Task list container |
253
142
  | `Task` | `- [ ]` / `- [x]` task item (`done` prop) |
254
143
  | `Table` | Markdown table |
255
144
  | `Tr` | Table row |
256
- | `Th` | Table header cell |
145
+ | `Th` | Table header cell (`align`: `left`, `center`, `right`) |
257
146
  | `Td` | Table data cell |
258
147
  | `Bold` | `**bold**` |
259
148
  | `Code` | `` `inline code` `` |
260
149
  | `Italic` | `*italic*` |
261
150
  | `Strikethrough` | `~~strikethrough~~` |
151
+ | `Br` | Hard line break (` \n` — two trailing spaces) |
152
+ | `Sup` | `<sup>content</sup>` superscript |
153
+ | `Sub` | `<sub>content</sub>` subscript |
154
+ | `Kbd` | `<kbd>content</kbd>` keyboard key |
155
+ | `Escape` | Escapes CommonMark metacharacters in children |
262
156
  | `Link` | `[text](url)` |
263
157
  | `Img` | `![alt](src)` |
264
- | `Md` | Raw markdown passthrough (escape hatch for complex inline combinations) |
265
- | `HtmlComment` | `<!-- comment -->` (invisible to most renderers; useful for LLM-only instructions) |
158
+ | `Md` | Raw Markdown passthrough renders verbatim, no transformation |
159
+ | `HtmlComment` | `<!-- comment -->` invisible to most renderers, useful for LLM-only instructions |
266
160
  | `Details` | `<details><summary>` collapsible block |
267
- | `Callout` | GitHub-style admonition with `type` prop: `note`, `tip`, `important`, `warning`, `caution` |
161
+ | `Callout` | GitHub-style admonition (`type`: `note`, `tip`, `important`, `warning`, `caution`) |
268
162
 
269
163
  ---
270
164
 
271
- ## Roadmap
165
+ ## Utilities
272
166
 
273
- **v0.1.0** current. TypeScript source, Bun-first. Markdown primitives + XML intrinsic elements.
167
+ ### `escapeMarkdown(s: string): string`
274
168
 
275
- **v0.2.0** two additions:
169
+ Escapes all CommonMark ASCII punctuation metacharacters with a backslash so user-supplied strings are treated as literal text by any markdown renderer.
276
170
 
277
- - _Context API (public)._ `DepthContext` already uses an internal context system for list nesting. v0.2.0 exposes this as a first-class public API. The pattern is React-familiar but synchronous and string-based.
278
-
279
- Should fix prop-drilling. A prompt tree with conditional sections per harness ends up threading `harness` through every component:
280
-
281
- ```tsx
282
- // v0.1.0 — prop-drilling
283
- const Prompt = ({ harness }: Props) => (
284
- <>
285
- <Instructions harness={harness} />
286
- <Rules harness={harness} />
287
- <Examples harness={harness} />
288
- </>
289
- )
290
- ```
291
-
292
- With the v0.2.0 context API, set it once at the root and read it anywhere:
171
+ ```tsx
172
+ import { escapeMarkdown, Escape, P } from "@theseus.run/jsx-md";
293
173
 
294
- ```tsx
295
- // v0.2.0 context
296
- const HarnessContext = createContext<'opencode' | 'copilot'>('copilot')
174
+ // Function form
175
+ const safe = escapeMarkdown(untrustedInput); // "**bold**" "\\*\\*bold\\*\\*"
297
176
 
298
- const prompt = render(
299
- <HarnessContext.Provider value="opencode">
300
- <Prompt />
301
- </HarnessContext.Provider>
302
- )
177
+ // Component form — same result, composable in JSX
178
+ render(<P>File: <Escape>{untrustedFilename}</Escape></P>);
179
+ ```
303
180
 
304
- // In any component, at any depth no prop threading:
305
- const Instructions = () => {
306
- const harness = useContext(HarnessContext)
307
- return harness === 'opencode'
308
- ? <P>Use task() for subtasks.</P>
309
- : null
310
- }
311
- ```
181
+ Escaped characters: `` \ ` * _ [ ] ( ) # + - . ! | ~ < > ``
312
182
 
313
- - _Node.js compiled output._ Ships a compiled `dist/` alongside the TypeScript source. Unblocks bare Node.js without a bundler.
183
+ `&` is intentionally not escaped use `escapeHtmlContent` for HTML contexts.
314
184
 
315
185
  ---
316
186
 
317
187
  ## License
318
188
 
319
- MIT — see [LICENSE](../../LICENSE).
320
-
321
- Built by [Roman Dubinin](https://romanonthego.com).
189
+ MIT — see [LICENSE](../../LICENSE).
322
190
 
323
- Part of [Theseus](https://theseus.run).
191
+ Built by [Roman Dubinin](https://romanonthego.dev). Part of [Theseus](https://theseus.run).
@@ -0,0 +1,11 @@
1
+ // src/jsx-runtime.ts
2
+ var Fragment = Symbol("Fragment");
3
+ function jsx(type, props) {
4
+ return { type, props };
5
+ }
6
+ var jsxs = jsx;
7
+
8
+ export { Fragment, jsx, jsxs };
9
+
10
+ //# debugId=A10E4D57EB3603AA64756E2164756E21
11
+ //# sourceMappingURL=index-8tdwjkh9.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/jsx-runtime.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Custom JSX-to-VNode runtime for agent markdown generation.\n *\n * Bun's JSX compilation with `jsxImportSource: \"@theseus.run/jsx-md\"` resolves\n * `jsx` and `jsxs` from `@theseus.run/jsx-md/jsx-runtime`. The factory builds\n * a VNode tree — no evaluation at construction time.\n *\n * Call `render(node)` from `./render.ts` to produce the final markdown string.\n * All markdown primitives live in primitives.tsx as named components.\n *\n * Fragment is a Symbol. render.ts imports this Symbol and handles it explicitly,\n * keeping the dependency one-way: render.ts → jsx-runtime.ts (no cycle).\n */\n\n// ---------------------------------------------------------------------------\n// VNode types\n// ---------------------------------------------------------------------------\n\n/**\n * The concrete runtime shape of a JSX element — what the `jsx()` factory always produces.\n *\n * Useful for structural inspection of a VNode tree (e.g. testing whether a node is an\n * element rather than a string, null, or array). The `isVNodeElement` predicate in\n * render.ts narrows to this type.\n *\n * @remarks **Do not use as a component return-type annotation.** TypeScript infers\n * `JSX.Element` (= `VNode`) as the return type of JSX expressions, not `VNodeElement`.\n * Annotating a component as `(): VNodeElement` causes TS2322 because `VNode` (the\n * inferred type) is not assignable to the narrower `VNodeElement`. Use `VNode` instead:\n * ```ts\n * // Wrong — TS2322\n * function MyComp(): VNodeElement { return <P>hi</P>; }\n * // Correct\n * function MyComp(): VNode { return <P>hi</P>; }\n * ```\n */\nexport type VNodeElement = {\n readonly type: Component | string | typeof Fragment;\n readonly props: Record<string, unknown>;\n};\n\nexport type VNode =\n | null\n | undefined\n | boolean\n | string\n | number\n | VNodeElement\n | ReadonlyArray<VNode>;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Component<P = any> = (props: P) => VNode;\n\n// ---------------------------------------------------------------------------\n// JSX namespace\n// ---------------------------------------------------------------------------\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n /**\n * JSX.Element is VNode so that components returning any valid VNode\n * (string, VNodeElement, Fragment, null, etc.) satisfy TypeScript's\n * component return-type check. The jsx() factory always produces\n * VNodeElement at runtime; the wider union is only for type-checking.\n */\n export type Element = VNode;\n\n // Catch-all — allows arbitrary lowercase XML tags as intrinsic elements.\n export interface IntrinsicElements {\n [tag: string]: { children?: VNode; [attr: string]: unknown };\n }\n\n export interface ElementChildrenAttribute {\n children: VNode;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Fragment symbol\n// ---------------------------------------------------------------------------\n\n/**\n * Fragment — a unique Symbol used as the `type` of JSX fragment VNodes.\n * render.ts detects this Symbol and renders children directly, with no wrapper.\n * Using a Symbol (rather than a function) eliminates the circular dependency\n * that previously required _render-registry.ts.\n */\nexport const Fragment = Symbol('Fragment');\n\n// ---------------------------------------------------------------------------\n// JSX factory\n// ---------------------------------------------------------------------------\n\n/**\n * JSX factory — called by Bun's compiled JSX. Builds VNode tree; no evaluation.\n *\n * `type` is a function component, a string tag name, or the Fragment symbol.\n * String tags are rendered as XML blocks by render.ts: `<tag attrs>\\ncontent\\n</tag>\\n`\n * (or self-closing when the inner content is empty).\n */\nexport function jsx(\n type: Component | string | typeof Fragment,\n props: Record<string, unknown>,\n): VNodeElement {\n return { type, props };\n}\n\n/** jsxs — same as jsx, used when there are multiple children (children is array) */\nexport const jsxs = jsx;\n"
6
+ ],
7
+ "mappings": ";AAuFO,IAAM,WAAW,OAAO,UAAU;AAalC,SAAS,GAAG,CACjB,MACA,OACc;AAAA,EACd,OAAO,EAAE,MAAM,MAAM;AAAA;AAIhB,IAAM,OAAO;",
8
+ "debugId": "A10E4D57EB3603AA64756E2164756E21",
9
+ "names": []
10
+ }