runline 0.7.2 → 0.7.5

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.
@@ -5,8 +5,10 @@ import { loadAllPlugins } from "../plugin/loader.js";
5
5
  import { registry } from "../plugin/registry.js";
6
6
  import { printError, printJson } from "../utils/output.js";
7
7
  export async function exec(code, options) {
8
- await loadAllPlugins();
9
8
  const config = loadConfig();
9
+ await loadAllPlugins({
10
+ builtinAllowlist: new Set(config.connections.map((c) => c.plugin)),
11
+ });
10
12
  const engine = new ExecutionEngine(registry, config);
11
13
  if (options.file) {
12
14
  if (!existsSync(code)) {
@@ -1,14 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { getQuickJS, shouldInterruptAfterDeadline, } from "quickjs-emscripten";
3
3
  import { applyEnvOverrides, updateConnectionConfig } from "../config/loader.js";
4
- import { registerNodePlugin } from "../plugin/node-plugin.js";
5
4
  export class ExecutionEngine {
6
5
  registry;
7
6
  config;
8
7
  constructor(registry, config) {
9
8
  this.registry = registry;
10
9
  this.config = config;
11
- registerNodePlugin(this.registry);
12
10
  }
13
11
  async execute(code, options) {
14
12
  const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
package/dist/main.js CHANGED
@@ -39,7 +39,7 @@ program
39
39
  .addHelpText("after", `
40
40
  The code runs in a QuickJS runtime with an \`actions\` proxy.
41
41
  Each installed plugin is a top-level global. Dot-chain into resource and action.
42
- The built-in \`node\` global exposes host-backed fs/path/os/process/crypto/fetch actions.
42
+ Configure the built-in \`node\` plugin to expose host-backed fs/path/os/process/crypto/fetch actions.
43
43
 
44
44
  Examples:
45
45
  $ runline exec 'return await docker.containers.list()'
@@ -32,4 +32,4 @@ export declare function discoverPlugins(configDir?: string | null, options?: Dis
32
32
  * Load all plugins and register them into the global registry.
33
33
  * Used by the CLI.
34
34
  */
35
- export declare function loadAllPlugins(): Promise<void>;
35
+ export declare function loadAllPlugins(options?: DiscoverOptions): Promise<void>;
@@ -1,12 +1,21 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createJiti } from "jiti";
5
6
  import { findConfigDir } from "../config/loader.js";
6
7
  import { resolvePluginExport } from "./api.js";
7
- import { registerNodePlugin } from "./node-plugin.js";
8
8
  import { registry } from "./registry.js";
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const jiti = createJiti(import.meta.url, {
11
+ moduleCache: false,
12
+ interopDefault: true,
13
+ tryNative: false,
14
+ alias: {
15
+ runline: new URL("../index.js", import.meta.url).pathname,
16
+ "runline/utils/cli": new URL("../utils/cli.js", import.meta.url).pathname,
17
+ },
18
+ });
10
19
  export async function loadPluginFromPath(path) {
11
20
  let absPath = resolve(path);
12
21
  if (existsSync(absPath) && statSync(absPath).isDirectory()) {
@@ -35,9 +44,16 @@ export async function loadPluginFromPath(path) {
35
44
  throw new Error(`No entry point found in ${absPath}`);
36
45
  }
37
46
  }
38
- const mod = await import(pathToFileURL(absPath).href);
47
+ // Plugins may live outside the project/package tree. Load them
48
+ // through Runline's module context so documented imports like
49
+ // `from "runline"` work without requiring a node_modules folder next
50
+ // to the plugin file.
51
+ const mod = (await jiti.import(absPath));
39
52
  const pluginId = absPath.replace(/.*\//, "").replace(/\.(ts|js)$/, "");
40
- return resolvePluginExport(mod.default, pluginId);
53
+ const pluginExport = typeof mod === "object" && mod !== null && "default" in mod
54
+ ? mod.default
55
+ : mod;
56
+ return resolvePluginExport(pluginExport, pluginId);
41
57
  }
42
58
  async function loadFromDirectory(dir) {
43
59
  const plugins = [];
@@ -163,11 +179,10 @@ export async function discoverPlugins(configDir, options = {}) {
163
179
  * Load all plugins and register them into the global registry.
164
180
  * Used by the CLI.
165
181
  */
166
- export async function loadAllPlugins() {
182
+ export async function loadAllPlugins(options = {}) {
167
183
  const configDir = findConfigDir();
168
- const plugins = await discoverPlugins(configDir);
184
+ const plugins = await discoverPlugins(configDir, options);
169
185
  for (const p of plugins) {
170
186
  registry.register(p);
171
187
  }
172
- registerNodePlugin(registry);
173
188
  }
@@ -77,6 +77,7 @@ async function paginateAll(ctx, path, key, qs) {
77
77
  }
78
78
  // ─── MIME encoding ───────────────────────────────────────────────
79
79
  const CRLF = "\r\n";
80
+ const GMAIL_MAX_RAW_BYTES = 35 * 1024 * 1024;
80
81
  function base64url(bytes) {
81
82
  const buf = typeof bytes === "string" ? Buffer.from(bytes, "utf-8") : Buffer.from(bytes);
82
83
  return buf
@@ -104,21 +105,57 @@ function header(name, value) {
104
105
  return "";
105
106
  return `${name}: ${value}${CRLF}`;
106
107
  }
108
+ function foldedBase64ByteLength(length) {
109
+ return length === 0 ? 0 : length + Math.floor((length - 1) / 76) * CRLF.length;
110
+ }
111
+ function foldBase64(encoded) {
112
+ let folded = "";
113
+ for (let i = 0; i < encoded.length; i += 76) {
114
+ if (i > 0)
115
+ folded += CRLF;
116
+ folded += encoded.slice(i, i + 76);
117
+ }
118
+ return folded;
119
+ }
120
+ function assertAttachmentPayloadFits(atts) {
121
+ const attachmentBodyBytes = atts.reduce((sum, att) => sum + foldedBase64ByteLength(att.contentBase64.length), 0);
122
+ if (attachmentBodyBytes > GMAIL_MAX_RAW_BYTES) {
123
+ throw new Error(`gmail: attachment payload is ${attachmentBodyBytes} bytes after MIME folding; Gmail API raw messages must be <= ${GMAIL_MAX_RAW_BYTES} bytes`);
124
+ }
125
+ }
107
126
  function textPart(body, mimeType) {
108
127
  const encoded = Buffer.from(body, "utf-8").toString("base64");
109
- // Fold base64 to 76 chars per RFC 2045.
110
- const folded = encoded.match(/.{1,76}/g)?.join(CRLF) ?? encoded;
111
128
  return (`Content-Type: ${mimeType}; charset="UTF-8"${CRLF}` +
112
129
  `Content-Transfer-Encoding: base64${CRLF}${CRLF}` +
113
- `${folded}${CRLF}`);
130
+ `${foldBase64(encoded)}${CRLF}`);
131
+ }
132
+ function normalizeAttachment(input, index) {
133
+ if (!input || typeof input !== "object") {
134
+ throw new Error(`gmail: attachment ${index} must be an object`);
135
+ }
136
+ const content = input.contentBase64;
137
+ const contentBase64 = typeof content === "string"
138
+ ? content
139
+ : content &&
140
+ typeof content === "object" &&
141
+ typeof content.contentBase64 === "string"
142
+ ? content.contentBase64
143
+ : undefined;
144
+ if (typeof contentBase64 !== "string") {
145
+ throw new Error(`gmail: attachment ${index} contentBase64 must be a base64 string`);
146
+ }
147
+ return {
148
+ name: input.name ?? input.filename ?? `attachment-${index + 1}`,
149
+ mimeType: input.mimeType ?? "application/octet-stream",
150
+ contentBase64,
151
+ };
114
152
  }
115
153
  function attachmentPart(att) {
116
154
  const encodedName = encodeHeaderWord(att.name);
117
- const folded = att.contentBase64.match(/.{1,76}/g)?.join(CRLF) ?? att.contentBase64;
118
155
  return (`Content-Type: ${att.mimeType}; name="${encodedName}"${CRLF}` +
119
156
  `Content-Disposition: attachment; filename="${encodedName}"${CRLF}` +
120
157
  `Content-Transfer-Encoding: base64${CRLF}${CRLF}` +
121
- `${folded}${CRLF}`);
158
+ `${foldBase64(att.contentBase64)}${CRLF}`);
122
159
  }
123
160
  /**
124
161
  * Build a MIME message and return its base64url-encoded form,
@@ -143,7 +180,8 @@ export function encodeEmail(email) {
143
180
  headers.push(header("MIME-Version", "1.0"));
144
181
  const text = email.text ?? "";
145
182
  const html = email.html ?? "";
146
- const atts = email.attachments ?? [];
183
+ const atts = (email.attachments ?? []).map((att, index) => normalizeAttachment(att, index));
184
+ assertAttachmentPayloadFits(atts);
147
185
  const hasAtt = atts.length > 0;
148
186
  const hasBoth = text && html;
149
187
  let bodyBlock;
@@ -189,6 +227,10 @@ export function encodeEmail(email) {
189
227
  `--${mixedBoundary}--${CRLF}`;
190
228
  }
191
229
  const raw = `${headers.join("")}${rootType}${bodyBlock}`;
230
+ const rawBytes = Buffer.byteLength(raw, "utf-8");
231
+ if (rawBytes > GMAIL_MAX_RAW_BYTES) {
232
+ throw new Error(`gmail: encoded message is ${rawBytes} bytes; Gmail API raw messages must be <= ${GMAIL_MAX_RAW_BYTES} bytes`);
233
+ }
192
234
  return base64url(raw);
193
235
  }
194
236
  // ─── Email address helpers ───────────────────────────────────────
@@ -351,7 +393,11 @@ function simplifyMessage(raw, labels) {
351
393
  }
352
394
  }
353
395
  // Body + attachments only meaningful when format=full was used.
354
- const acc = { text: [], html: [], attachments: [] };
396
+ const acc = {
397
+ text: [],
398
+ html: [],
399
+ attachments: [],
400
+ };
355
401
  walkPayload(payload, acc);
356
402
  if (acc.text.length > 0)
357
403
  out.text = acc.text.join("\n");
@@ -412,7 +458,9 @@ async function replyToMessage(ctx, messageId, p) {
412
458
  to,
413
459
  cc: normalizeAddressList(p.cc),
414
460
  bcc: normalizeAddressList(p.bcc),
415
- subject: subject.toLowerCase().startsWith("re:") ? subject : `Re: ${subject}`,
461
+ subject: subject.toLowerCase().startsWith("re:")
462
+ ? subject
463
+ : `Re: ${subject}`,
416
464
  text: p.text,
417
465
  html: p.html,
418
466
  inReplyTo: messageIdHeader,
@@ -756,7 +804,11 @@ export default function gmail(rl) {
756
804
  description: "Get a thread by ID",
757
805
  inputSchema: {
758
806
  id: { type: "string", required: true },
759
- format: { type: "string", required: false, description: "minimal | full | metadata" },
807
+ format: {
808
+ type: "string",
809
+ required: false,
810
+ description: "minimal | full | metadata",
811
+ },
760
812
  metadataHeaders: { type: "array", required: false },
761
813
  simple: {
762
814
  type: "boolean",
@@ -996,10 +1048,12 @@ export default function gmail(rl) {
996
1048
  async execute(input, ctx) {
997
1049
  const p = (input ?? {});
998
1050
  const body = { name: p.name };
999
- if (p.labelListVisibility)
1051
+ if (p.labelListVisibility) {
1000
1052
  body.labelListVisibility = p.labelListVisibility;
1001
- if (p.messageListVisibility)
1053
+ }
1054
+ if (p.messageListVisibility) {
1002
1055
  body.messageListVisibility = p.messageListVisibility;
1056
+ }
1003
1057
  return gmailRequest(ctx, "POST", "/labels", body);
1004
1058
  },
1005
1059
  });
@@ -1039,10 +1093,12 @@ export default function gmail(rl) {
1039
1093
  const body = {};
1040
1094
  if (p.name)
1041
1095
  body.name = p.name;
1042
- if (p.labelListVisibility)
1096
+ if (p.labelListVisibility) {
1043
1097
  body.labelListVisibility = p.labelListVisibility;
1044
- if (p.messageListVisibility)
1098
+ }
1099
+ if (p.messageListVisibility) {
1045
1100
  body.messageListVisibility = p.messageListVisibility;
1101
+ }
1046
1102
  return gmailRequest(ctx, "PATCH", `/labels/${p.id}`, body);
1047
1103
  },
1048
1104
  });