@theseus.run/jsx-md 0.1.2 → 0.2.1

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,17 @@ 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
21
+ ```
22
+
23
+ **AI coding agent skill** (OpenCode, Cursor, Copilot, Claude Code):
24
+
25
+ ```bash
26
+ npx skills add https://github.com/theseus-run/theseus/tree/master/packages/jsx-md
86
27
  ```
87
28
 
88
- Then in `tsconfig.json`:
29
+ `tsconfig.json`:
89
30
 
90
31
  ```json
91
32
  {
@@ -96,84 +37,71 @@ Then in `tsconfig.json`:
96
37
  }
97
38
  ```
98
39
 
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.
40
+ 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
41
 
115
42
  ---
116
43
 
117
44
  ## Usage
118
45
 
119
46
  ```tsx
120
- // system-prompt.tsx
121
47
  import { render, H2, P, Ul, Li, Bold, Code } from "@theseus.run/jsx-md";
122
48
 
123
- const prompt = render(
49
+ const ReviewerPrompt = ({ repo }: { repo: string }) => (
124
50
  <>
125
51
  <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>
52
+ <P>You are a precise code reviewer. Find bugs, not style issues.</P>
130
53
 
131
54
  <H2>Rules</H2>
132
55
  <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>
56
+ <Li>Flag <Bold>P0</Bold> issues first — do not bury them.</Li>
139
57
  <Li>One finding per comment. No compound observations.</Li>
58
+ <Li>Use <Code>inline code</Code> when referencing identifiers.</Li>
140
59
  </Ul>
141
-
142
- <H2>Output format</H2>
143
- <P>
144
- Respond with a structured list. Each item: severity, location, finding.
145
- </P>
146
60
  </>
147
61
  );
148
62
 
149
- console.log(prompt);
63
+ const prompt = render(<ReviewerPrompt repo="cockpit" />);
64
+ // "## Role\n\nYou are a precise code reviewer..."
150
65
  ```
151
66
 
152
- Output:
67
+ `render()` returns a plain string. No virtual DOM, no React runtime. Same input, same string, every time.
68
+
69
+ ---
153
70
 
154
- ```markdown
155
- ## Role
71
+ ## Context API
156
72
 
157
- You are a precise code reviewer. Your job is to find bugs, not suggest style changes.
73
+ Avoids prop-drilling through shared fragment trees. Same shape as React `createContext`, `useContext`, `Context.Provider` synchronous, no rules-of-hooks.
158
74
 
159
- ## Rules
75
+ ```tsx
76
+ import { render, createContext, useContext, Ul, Li, Code } from "@theseus.run/jsx-md";
160
77
 
161
- - Flag **P0** issues immediately — do not bury them.
162
- - Use `inline code` when referencing identifiers.
163
- - One finding per comment. No compound observations.
78
+ const HarnessCtx = createContext<'opencode' | 'copilot'>('copilot');
164
79
 
165
- ## Output format
80
+ const StepsSection = () => {
81
+ const harness = useContext(HarnessCtx);
82
+ return (
83
+ <Ul>
84
+ <Li>Always verify output before claiming done.</Li>
85
+ {harness === 'opencode' && <Li>Use <Code>task()</Code> for multi-step subtasks.</Li>}
86
+ </Ul>
87
+ );
88
+ };
166
89
 
167
- Respond with a structured list. Each item: severity, location, finding.
90
+ // Wire it once at the root no prop threading:
91
+ const prompt = render(
92
+ <HarnessCtx.Provider value="opencode">
93
+ <StepsSection />
94
+ </HarnessCtx.Provider>
95
+ );
168
96
  ```
169
97
 
170
- ---
98
+ `useContext` returns the default when called outside a Provider. Providers nest — innermost wins.
171
99
 
172
- ## Structured agent instructions
100
+ ---
173
101
 
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.
102
+ ## XML intrinsics
175
103
 
176
- Any lowercase JSX tag is an XML intrinsic in `@theseus.run/jsx-md`. No imports, no registration:
104
+ Any lowercase JSX tag renders as an XML block. No imports, no registration:
177
105
 
178
106
  ```tsx
179
107
  const ReviewerPrompt = ({ repo, examples }: Props) => (
@@ -185,13 +113,6 @@ const ReviewerPrompt = ({ repo, examples }: Props) => (
185
113
  <instructions>
186
114
  <H2>Role</H2>
187
115
  <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
116
  </instructions>
196
117
 
197
118
  {examples.length > 0 && (
@@ -204,120 +125,73 @@ const ReviewerPrompt = ({ repo, examples }: Props) => (
204
125
  </examples>
205
126
  )}
206
127
  </>
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>
128
+ );
234
129
  ```
235
130
 
236
- Attributes are typed (`index={1}` `index="1"`), boolean `true` attrs render bare, `false`/`null`/`undefined` are omitted. Empty tags self-close: `<tag />`.
131
+ Attributes are typed `index={1}` serializes to `index="1"`. Boolean `true` renders bare, `false`/`null`/`undefined` are omitted. Empty tags self-close.
237
132
 
238
133
  ---
239
134
 
240
135
  ## Primitives
241
136
 
242
- | Component | Markdown output |
243
- |-----------|----------------|
137
+ | Component | Output |
138
+ |---|---|
244
139
  | `H1`–`H6` | `#`–`######` headings |
245
140
  | `P` | Paragraph (blank line separated) |
246
141
  | `Hr` | `---` horizontal rule |
247
- | `Codeblock` | Fenced code block with optional `lang` prop |
142
+ | `Codeblock` | Fenced code block (`lang` prop optional) |
248
143
  | `Blockquote` | `>` blockquote |
249
144
  | `Ul` | Unordered list |
250
- | `Ol` | Ordered list |
251
- | `Li` | List item (supports nesting via `Ul`/`Ol` inside `Li`) |
145
+ | `Ol` | Ordered list (auto-numbered, nesting supported) |
146
+ | `Li` | List item (supports nested `Ul` or `Ol` inside `Li`) |
252
147
  | `TaskList` | Task list container |
253
148
  | `Task` | `- [ ]` / `- [x]` task item (`done` prop) |
254
149
  | `Table` | Markdown table |
255
150
  | `Tr` | Table row |
256
- | `Th` | Table header cell |
151
+ | `Th` | Table header cell (`align`: `left`, `center`, `right`) |
257
152
  | `Td` | Table data cell |
258
153
  | `Bold` | `**bold**` |
259
154
  | `Code` | `` `inline code` `` |
260
155
  | `Italic` | `*italic*` |
261
156
  | `Strikethrough` | `~~strikethrough~~` |
157
+ | `Br` | Hard line break (` \n` — two trailing spaces) |
158
+ | `Sup` | `<sup>content</sup>` superscript |
159
+ | `Sub` | `<sub>content</sub>` subscript |
160
+ | `Kbd` | `<kbd>content</kbd>` keyboard key |
161
+ | `Escape` | Escapes CommonMark metacharacters in children |
262
162
  | `Link` | `[text](url)` |
263
163
  | `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) |
164
+ | `Md` | Raw Markdown passthrough renders verbatim, no transformation |
165
+ | `HtmlComment` | `<!-- comment -->` invisible to most renderers, useful for LLM-only instructions |
266
166
  | `Details` | `<details><summary>` collapsible block |
267
- | `Callout` | GitHub-style admonition with `type` prop: `note`, `tip`, `important`, `warning`, `caution` |
167
+ | `Callout` | GitHub-style admonition (`type`: `note`, `tip`, `important`, `warning`, `caution`) |
268
168
 
269
169
  ---
270
170
 
271
- ## Roadmap
272
-
273
- **v0.1.0** — current. TypeScript source, Bun-first. Markdown primitives + XML intrinsic elements.
274
-
275
- **v0.2.0** — two additions:
276
-
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.
171
+ ## Utilities
278
172
 
279
- Should fix prop-drilling. A prompt tree with conditional sections per harness ends up threading `harness` through every component:
173
+ ### `escapeMarkdown(s: string): string`
280
174
 
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
- ```
175
+ Escapes all CommonMark ASCII punctuation metacharacters with a backslash so user-supplied strings are treated as literal text by any markdown renderer.
291
176
 
292
- With the v0.2.0 context API, set it once at the root and read it anywhere:
177
+ ```tsx
178
+ import { escapeMarkdown, Escape, P } from "@theseus.run/jsx-md";
293
179
 
294
- ```tsx
295
- // v0.2.0 context
296
- const HarnessContext = createContext<'opencode' | 'copilot'>('copilot')
180
+ // Function form
181
+ const safe = escapeMarkdown(untrustedInput); // "**bold**" "\\*\\*bold\\*\\*"
297
182
 
298
- const prompt = render(
299
- <HarnessContext.Provider value="opencode">
300
- <Prompt />
301
- </HarnessContext.Provider>
302
- )
183
+ // Component form — same result, composable in JSX
184
+ render(<P>File: <Escape>{untrustedFilename}</Escape></P>);
185
+ ```
303
186
 
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
- ```
187
+ Escaped characters: `` \ ` * _ [ ] ( ) # + - . ! | ~ < > ``
312
188
 
313
- - _Node.js compiled output._ Ships a compiled `dist/` alongside the TypeScript source. Unblocks bare Node.js without a bundler.
189
+ `&` is intentionally not escaped use `escapeHtmlContent` for HTML contexts.
314
190
 
315
191
  ---
316
192
 
317
193
  ## License
318
194
 
319
- MIT — see [LICENSE](../../LICENSE).
320
-
321
- Built by [Roman Dubinin](https://romanonthego.com).
195
+ MIT — see [LICENSE](../../LICENSE).
322
196
 
323
- Part of [Theseus](https://theseus.run).
197
+ 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
+ }