runline 0.15.1 → 0.16.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.
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render agent-authored React Email JSX into wire-ready email HTML
|
|
3
|
+
* plus a plain-text alternative.
|
|
4
|
+
*
|
|
5
|
+
* Why JSX instead of hand-written HTML: email-grade HTML (table
|
|
6
|
+
* layouts, inlined styles, client quirks) is exactly what agents get
|
|
7
|
+
* wrong; @react-email/components encodes that knowledge, and agents
|
|
8
|
+
* write JSX fluently. Evaluating agent-supplied JSX adds no new risk
|
|
9
|
+
* class — runline's executor is already a coding surface.
|
|
10
|
+
*
|
|
11
|
+
* All heavy dependencies (react, sucrase, react-email) are optional
|
|
12
|
+
* peers of runline — like playwright for browser actions — loaded
|
|
13
|
+
* lazily on first use. Hosts that never render JSX pay nothing;
|
|
14
|
+
* hosts without the packages get an install hint instead of a
|
|
15
|
+
* module-resolution stack trace.
|
|
16
|
+
*/
|
|
17
|
+
const PEER_INSTALL_HINT = "jsx email bodies require optional peer packages. Install them in the host project: " +
|
|
18
|
+
"react react-dom @react-email/components @react-email/render sucrase. " +
|
|
19
|
+
"Alternatively pass `html` or `text` instead of `jsx`.";
|
|
20
|
+
let modulesPromise;
|
|
21
|
+
async function loadModules() {
|
|
22
|
+
modulesPromise ??= Promise.all([
|
|
23
|
+
import("react"),
|
|
24
|
+
import("@react-email/components"),
|
|
25
|
+
import("@react-email/render"),
|
|
26
|
+
import("sucrase"),
|
|
27
|
+
]).then(([react, components, renderMod, sucrase]) => ({
|
|
28
|
+
React: react.default ?? react,
|
|
29
|
+
components: components,
|
|
30
|
+
render: renderMod.render,
|
|
31
|
+
transform: sucrase.transform,
|
|
32
|
+
}), (err) => {
|
|
33
|
+
// Don't cache the failure — the host can install the peers and
|
|
34
|
+
// retry without restarting the process.
|
|
35
|
+
modulesPromise = undefined;
|
|
36
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
+
throw new Error(`${PEER_INSTALL_HINT} (${msg})`);
|
|
38
|
+
});
|
|
39
|
+
return modulesPromise;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Components become named parameters of the evaluation function, so
|
|
43
|
+
* only exports that are valid JS identifiers (the React components —
|
|
44
|
+
* capitalized) can be exposed. Lowercase utility exports are omitted
|
|
45
|
+
* on purpose: the JSX surface is components-only.
|
|
46
|
+
*/
|
|
47
|
+
function componentScope(components) {
|
|
48
|
+
const names = [];
|
|
49
|
+
const values = [];
|
|
50
|
+
for (const [key, value] of Object.entries(components)) {
|
|
51
|
+
if (!/^[A-Z][A-Za-z0-9_$]*$/.test(key))
|
|
52
|
+
continue;
|
|
53
|
+
names.push(key);
|
|
54
|
+
values.push(value);
|
|
55
|
+
}
|
|
56
|
+
return { names, values };
|
|
57
|
+
}
|
|
58
|
+
export async function renderEmailJsx(jsx) {
|
|
59
|
+
const source = jsx.trim();
|
|
60
|
+
if (!source)
|
|
61
|
+
throw new Error("jsx body is empty");
|
|
62
|
+
const { React, components, render, transform } = await loadModules();
|
|
63
|
+
let compiled;
|
|
64
|
+
try {
|
|
65
|
+
// Wrap as an expression statement so a bare `<Html>…</Html>` is a
|
|
66
|
+
// valid program for sucrase. The parentheses nudge the surface
|
|
67
|
+
// toward a single expression (statement forms like `const x = 1`
|
|
68
|
+
// fail to parse) — a shape hint, not a boundary: the executor is
|
|
69
|
+
// already a coding surface, so escaping it grants nothing.
|
|
70
|
+
compiled = transform(`__element = (${source});`, {
|
|
71
|
+
transforms: ["jsx"],
|
|
72
|
+
production: true,
|
|
73
|
+
}).code;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
throw new Error(`jsx parse failed: ${msg}`);
|
|
78
|
+
}
|
|
79
|
+
const { names, values } = componentScope(components);
|
|
80
|
+
let element;
|
|
81
|
+
try {
|
|
82
|
+
const fn = new Function("React", ...names, `let __element; ${compiled}; return __element;`);
|
|
83
|
+
element = fn(React, ...values);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
throw new Error(`jsx evaluation failed: ${msg}. Available components: ${names.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
if (!React.isValidElement(element)) {
|
|
90
|
+
throw new Error("jsx must evaluate to a single React element");
|
|
91
|
+
}
|
|
92
|
+
const [html, text] = await Promise.all([
|
|
93
|
+
render(element),
|
|
94
|
+
render(element, { plainText: true }),
|
|
95
|
+
]);
|
|
96
|
+
return { html, text };
|
|
97
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* or parsed `payload` tree for format=full).
|
|
22
22
|
*/
|
|
23
23
|
import * as t from "typebox";
|
|
24
|
+
import { renderEmailJsx } from "../../_shared/emailJsx.js";
|
|
24
25
|
import { googleAccessToken } from "../../_shared/googleAuth.js";
|
|
25
26
|
import { GoogleTimestamp, Id, NonEmptyString, stringEnum, } from "../../_shared/googleSchemas.js";
|
|
26
27
|
// ─── Auth ────────────────────────────────────────────────────────
|
|
@@ -282,6 +283,32 @@ function normalizeAddressList(input) {
|
|
|
282
283
|
.map((p) => (p.includes("<") && p.includes(">") ? p : `<${p}>`))
|
|
283
284
|
.join(", ");
|
|
284
285
|
}
|
|
286
|
+
// ─── Body resolution ─────────────────────────────────────────────
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the outgoing body for send/reply/draft: `jsx` renders to
|
|
289
|
+
* html with a generated plain-text alternative; an explicit `text`
|
|
290
|
+
* wins over the generated fallback so every jsx email still ships
|
|
291
|
+
* multipart/alternative.
|
|
292
|
+
* Composing inputs carry no `html` field — jsx is the only HTML path
|
|
293
|
+
* (raw HTML embeds go through dangerouslySetInnerHTML inside jsx).
|
|
294
|
+
*/
|
|
295
|
+
async function resolveBody(p) {
|
|
296
|
+
let text = p.text;
|
|
297
|
+
let html;
|
|
298
|
+
if (typeof p.jsx === "string" && p.jsx.trim()) {
|
|
299
|
+
let rendered;
|
|
300
|
+
try {
|
|
301
|
+
rendered = await renderEmailJsx(p.jsx);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
305
|
+
throw new Error(`gmail: ${msg}`);
|
|
306
|
+
}
|
|
307
|
+
html = rendered.html;
|
|
308
|
+
text ??= rendered.text;
|
|
309
|
+
}
|
|
310
|
+
return { text, html };
|
|
311
|
+
}
|
|
285
312
|
function findHeader(payload, name) {
|
|
286
313
|
if (!payload?.headers)
|
|
287
314
|
return undefined;
|
|
@@ -444,6 +471,9 @@ async function replyToMessage(ctx, messageId, p) {
|
|
|
444
471
|
if (p.replyToSenderOnly && p.replyToRecipientsOnly) {
|
|
445
472
|
throw new Error("gmail: replyToSenderOnly and replyToRecipientsOnly are mutually exclusive");
|
|
446
473
|
}
|
|
474
|
+
// Render before any network round-trip so malformed jsx fails
|
|
475
|
+
// without spending Gmail API calls.
|
|
476
|
+
const { text, html } = await resolveBody(p);
|
|
447
477
|
const original = (await gmailRequest(ctx, "GET", `/messages/${messageId}`, undefined, { format: "metadata", metadataHeaders: REPLY_METADATA_HEADERS }));
|
|
448
478
|
const subject = findHeader(original.payload, "Subject") ?? "";
|
|
449
479
|
const messageIdHeader = findHeader(original.payload, "Message-ID") ?? "";
|
|
@@ -487,8 +517,8 @@ async function replyToMessage(ctx, messageId, p) {
|
|
|
487
517
|
subject: subject.toLowerCase().startsWith("re:")
|
|
488
518
|
? subject
|
|
489
519
|
: `Re: ${subject}`,
|
|
490
|
-
text
|
|
491
|
-
html
|
|
520
|
+
text,
|
|
521
|
+
html,
|
|
492
522
|
inReplyTo: messageIdHeader,
|
|
493
523
|
references: messageIdHeader,
|
|
494
524
|
attachments: p.attachments,
|
|
@@ -532,6 +562,9 @@ const Attachment = t.Object({
|
|
|
532
562
|
contentBase64: AttachmentContent,
|
|
533
563
|
}, strict);
|
|
534
564
|
const Attachments = t.Array(Attachment);
|
|
565
|
+
const Jsx = t.Optional(t.String({
|
|
566
|
+
description: 'React Email JSX expression rendered server-side into client-safe HTML with an auto-generated plain-text alternative — handles cross-client rendering (iOS Mail, Gmail, Outlook). For Hebrew/Arabic set dir on the root: `<Html dir="rtl" lang="he"><Container><Heading>שלום</Heading><Text>גוף ההודעה</Text><Button href="...">אישור</Button></Container></Html>`. To embed pre-rendered HTML from elsewhere: `<Html><Body><div dangerouslySetInnerHTML={{ __html: theHtml }} /></Body></Html>`.',
|
|
567
|
+
}));
|
|
535
568
|
const HasMessageContent = {
|
|
536
569
|
anyOf: [
|
|
537
570
|
{
|
|
@@ -539,8 +572,8 @@ const HasMessageContent = {
|
|
|
539
572
|
properties: { text: { type: "string", minLength: 1, pattern: "\\S" } },
|
|
540
573
|
},
|
|
541
574
|
{
|
|
542
|
-
required: ["
|
|
543
|
-
properties: {
|
|
575
|
+
required: ["jsx"],
|
|
576
|
+
properties: { jsx: { type: "string", minLength: 1, pattern: "\\S" } },
|
|
544
577
|
},
|
|
545
578
|
{
|
|
546
579
|
required: ["attachments"],
|
|
@@ -550,7 +583,7 @@ const HasMessageContent = {
|
|
|
550
583
|
};
|
|
551
584
|
const ReplyFields = {
|
|
552
585
|
text: t.Optional(t.String()),
|
|
553
|
-
|
|
586
|
+
jsx: Jsx,
|
|
554
587
|
cc: t.Optional(t.String()),
|
|
555
588
|
bcc: t.Optional(t.String()),
|
|
556
589
|
replyToSenderOnly: t.Optional(t.Boolean()),
|
|
@@ -695,12 +728,12 @@ export default function gmail(rl) {
|
|
|
695
728
|
// ── Message ───────────────────────────────────────────
|
|
696
729
|
rl.registerAction("message.send", {
|
|
697
730
|
access: "write",
|
|
698
|
-
description: "Send an email",
|
|
731
|
+
description: "Send an email. `text` for plain bodies; `jsx` (React Email) for formatted or RTL (Hebrew/Arabic) bodies — renders client-safe HTML with a text fallback.",
|
|
699
732
|
inputSchema: t.Object({
|
|
700
733
|
to: NonEmptyString,
|
|
701
734
|
subject: t.String(),
|
|
702
735
|
text: t.Optional(t.String({ description: "Plain body" })),
|
|
703
|
-
|
|
736
|
+
jsx: Jsx,
|
|
704
737
|
cc: t.Optional(t.String()),
|
|
705
738
|
bcc: t.Optional(t.String()),
|
|
706
739
|
replyTo: t.Optional(t.String()),
|
|
@@ -710,6 +743,7 @@ export default function gmail(rl) {
|
|
|
710
743
|
}, { ...strict, ...HasMessageContent }),
|
|
711
744
|
async execute(input, ctx) {
|
|
712
745
|
const p = (input ?? {});
|
|
746
|
+
const { text, html } = await resolveBody(p);
|
|
713
747
|
const email = {
|
|
714
748
|
to: normalizeAddressList(p.to),
|
|
715
749
|
cc: normalizeAddressList(p.cc),
|
|
@@ -717,8 +751,8 @@ export default function gmail(rl) {
|
|
|
717
751
|
replyTo: normalizeAddressList(p.replyTo),
|
|
718
752
|
from: p.from,
|
|
719
753
|
subject: p.subject ?? "",
|
|
720
|
-
text
|
|
721
|
-
html
|
|
754
|
+
text,
|
|
755
|
+
html,
|
|
722
756
|
attachments: p.attachments,
|
|
723
757
|
};
|
|
724
758
|
const body = { raw: encodeEmail(email) };
|
|
@@ -729,7 +763,7 @@ export default function gmail(rl) {
|
|
|
729
763
|
});
|
|
730
764
|
rl.registerAction("message.reply", {
|
|
731
765
|
access: "write",
|
|
732
|
-
description: "Reply to a message, preserving threadId and In-Reply-To/References headers",
|
|
766
|
+
description: "Reply to a message, preserving threadId and In-Reply-To/References headers. `text` for plain bodies; `jsx` (React Email) for formatted ones.",
|
|
733
767
|
inputSchema: t.Object({ messageId: Id, ...ReplyFields }, ReplyOptions),
|
|
734
768
|
async execute(input, ctx) {
|
|
735
769
|
const p = (input ?? {});
|
|
@@ -943,7 +977,7 @@ export default function gmail(rl) {
|
|
|
943
977
|
});
|
|
944
978
|
rl.registerAction("thread.reply", {
|
|
945
979
|
access: "write",
|
|
946
|
-
description: "Reply to the last message in a thread (convenience wrapper over message.reply)",
|
|
980
|
+
description: "Reply to the last message in a thread (convenience wrapper over message.reply). `text` for plain bodies; `jsx` (React Email) for formatted ones.",
|
|
947
981
|
inputSchema: t.Object({ id: Id, ...ReplyFields }, ReplyOptions),
|
|
948
982
|
async execute(input, ctx) {
|
|
949
983
|
const p = (input ?? {});
|
|
@@ -958,12 +992,12 @@ export default function gmail(rl) {
|
|
|
958
992
|
// ── Draft ─────────────────────────────────────────────
|
|
959
993
|
rl.registerAction("draft.create", {
|
|
960
994
|
access: "write",
|
|
961
|
-
description: "Create a draft",
|
|
995
|
+
description: "Create a draft. `text` for plain bodies; `jsx` (React Email) for formatted ones.",
|
|
962
996
|
inputSchema: t.Object({
|
|
963
997
|
to: t.Optional(t.String()),
|
|
964
998
|
subject: t.Optional(t.String()),
|
|
965
999
|
text: t.Optional(t.String()),
|
|
966
|
-
|
|
1000
|
+
jsx: Jsx,
|
|
967
1001
|
cc: t.Optional(t.String()),
|
|
968
1002
|
bcc: t.Optional(t.String()),
|
|
969
1003
|
replyTo: t.Optional(t.String()),
|
|
@@ -977,6 +1011,7 @@ export default function gmail(rl) {
|
|
|
977
1011
|
async execute(input, ctx) {
|
|
978
1012
|
const p = (input ?? {});
|
|
979
1013
|
const from = p.from ?? p.fromAlias;
|
|
1014
|
+
const { text, html } = await resolveBody(p);
|
|
980
1015
|
const email = {
|
|
981
1016
|
to: normalizeAddressList(p.to) ?? "",
|
|
982
1017
|
cc: normalizeAddressList(p.cc),
|
|
@@ -984,8 +1019,8 @@ export default function gmail(rl) {
|
|
|
984
1019
|
replyTo: normalizeAddressList(p.replyTo),
|
|
985
1020
|
from,
|
|
986
1021
|
subject: p.subject ?? "",
|
|
987
|
-
text
|
|
988
|
-
html
|
|
1022
|
+
text,
|
|
1023
|
+
html,
|
|
989
1024
|
attachments: p.attachments,
|
|
990
1025
|
};
|
|
991
1026
|
// When threading a draft, fetch the last Message-ID in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Code mode for agents — turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -63,11 +63,31 @@
|
|
|
63
63
|
"typescript": "^5.8.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"
|
|
66
|
+
"@react-email/components": "^1.0.12",
|
|
67
|
+
"@react-email/render": "^2.1.0",
|
|
68
|
+
"playwright": "^1.57.0",
|
|
69
|
+
"react": "^19.0.0",
|
|
70
|
+
"react-dom": "^19.0.0",
|
|
71
|
+
"sucrase": "^3.35.1"
|
|
67
72
|
},
|
|
68
73
|
"peerDependenciesMeta": {
|
|
74
|
+
"@react-email/components": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@react-email/render": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
69
80
|
"playwright": {
|
|
70
81
|
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"react": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"react-dom": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"sucrase": {
|
|
90
|
+
"optional": true
|
|
71
91
|
}
|
|
72
92
|
},
|
|
73
93
|
"dependencies": {
|