runline 0.7.7 → 0.8.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.
Files changed (41) hide show
  1. package/dist/commands/auth.js +4 -2
  2. package/dist/config/loader.d.ts +2 -4
  3. package/dist/config/loader.js +2 -1
  4. package/dist/core/engine.js +18 -13
  5. package/dist/index.d.ts +2 -2
  6. package/dist/plugin/api.d.ts +2 -9
  7. package/dist/plugin/api.js +1 -4
  8. package/dist/plugin/schema.d.ts +19 -0
  9. package/dist/plugin/schema.js +168 -0
  10. package/dist/plugin/types.d.ts +22 -8
  11. package/dist/plugins/_shared/imageFile.js +40 -0
  12. package/dist/plugins/_shared/microsoftAuth.js +100 -0
  13. package/dist/plugins/gmail/src/index.js +27 -3
  14. package/dist/plugins/googleAppsScript/src/index.js +203 -0
  15. package/dist/plugins/googleImage/src/index.js +30 -11
  16. package/dist/plugins/linear/src/attachments.js +65 -0
  17. package/dist/plugins/linear/src/comments.js +50 -0
  18. package/dist/plugins/linear/src/cycles.js +40 -0
  19. package/dist/plugins/linear/src/index.js +31 -1153
  20. package/dist/plugins/linear/src/initiatives.js +79 -0
  21. package/dist/plugins/linear/src/issues.js +245 -0
  22. package/dist/plugins/linear/src/labels.js +69 -0
  23. package/dist/plugins/linear/src/organization.js +12 -0
  24. package/dist/plugins/linear/src/projects.js +189 -0
  25. package/dist/plugins/linear/src/shared.js +131 -0
  26. package/dist/plugins/linear/src/states.js +39 -0
  27. package/dist/plugins/linear/src/teams.js +74 -0
  28. package/dist/plugins/linear/src/users.js +36 -0
  29. package/dist/plugins/linear/src/views.js +96 -0
  30. package/dist/plugins/linear/src/webhooks.js +57 -0
  31. package/dist/plugins/microsoftCalendar/src/index.js +46 -0
  32. package/dist/plugins/microsoftFiles/src/index.js +127 -0
  33. package/dist/plugins/microsoftMail/src/index.js +91 -0
  34. package/dist/plugins/openai/src/index.js +45 -20
  35. package/dist/plugins/parallel/src/index.js +100 -0
  36. package/dist/plugins/recraft/src/index.js +10 -6
  37. package/dist/plugins/replicate/src/index.js +17 -3
  38. package/dist/plugins/steel/src/index.js +378 -0
  39. package/dist/plugins/together/src/index.js +10 -6
  40. package/dist/plugins/xai/src/index.js +11 -5
  41. package/package.json +3 -2
@@ -0,0 +1,91 @@
1
+ import { graphRequest, microsoftSetupHelp, userBase } from "../../_shared/microsoftAuth.js";
2
+ const NAME = "microsoftMail";
3
+ const SCOPES = [
4
+ "https://graph.microsoft.com/Mail.Send",
5
+ "https://graph.microsoft.com/Mail.ReadWrite",
6
+ "https://graph.microsoft.com/Mail.Read",
7
+ ];
8
+ const recipients = (addrs) => (Array.isArray(addrs) ? addrs : addrs ? [addrs] : []).map((a) => ({
9
+ emailAddress: { address: a },
10
+ }));
11
+ function toMessage(input) {
12
+ return {
13
+ subject: input.subject,
14
+ body: { contentType: input.html ? "HTML" : "Text", content: input.body ?? "" },
15
+ toRecipients: recipients(input.to),
16
+ ccRecipients: recipients(input.cc),
17
+ };
18
+ }
19
+ export default function microsoftMail(rl) {
20
+ rl.setName(NAME);
21
+ rl.setVersion("1.0.0");
22
+ rl.setConnectionSchema({
23
+ tenantId: { type: "string", required: false, env: "MS_GRAPH_TENANT_ID", description: "Entra tenant id (app-only) or omit for OAuth /common" },
24
+ clientId: { type: "string", required: false, env: "MS_GRAPH_CLIENT_ID", description: "App (client) id" },
25
+ clientSecret: { type: "string", required: false, env: "MS_GRAPH_CLIENT_SECRET", description: "Client secret VALUE" },
26
+ refreshToken: { type: "string", required: false, env: "MICROSOFTMAIL_REFRESH_TOKEN", description: "OAuth2 refresh token (set by the login flow)" },
27
+ userUpn: { type: "string", required: false, env: "MS_GRAPH_USER_UPN", description: "App-only only: target mailbox UPN (e.g. agent@contoso.com)" },
28
+ });
29
+ rl.setOAuth({
30
+ authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
31
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
32
+ scopes: [...SCOPES, "offline_access"],
33
+ setupHelp: microsoftSetupHelp("Mail.Send, Mail.ReadWrite, Mail.Read"),
34
+ });
35
+ rl.registerAction("mail.send", {
36
+ description: "Send an email as the connected mailbox. Returns {success}. Get user approval before sending external mail.",
37
+ inputSchema: {
38
+ to: { type: "array", required: true, description: "Recipient address(es)" },
39
+ subject: { type: "string", required: true },
40
+ body: { type: "string", required: true },
41
+ cc: { type: "array", required: false },
42
+ html: { type: "boolean", required: false, description: "Body is HTML (default plain text)" },
43
+ },
44
+ async execute(input, ctx) {
45
+ await graphRequest(ctx, NAME, SCOPES, "POST", `${userBase(ctx)}/sendMail`, {
46
+ message: toMessage(input),
47
+ saveToSentItems: true,
48
+ });
49
+ return { success: true };
50
+ },
51
+ });
52
+ rl.registerAction("mail.draft", {
53
+ description: "Create a draft email (not sent). Returns {id, webLink}.",
54
+ inputSchema: {
55
+ to: { type: "array", required: false },
56
+ subject: { type: "string", required: true },
57
+ body: { type: "string", required: true },
58
+ cc: { type: "array", required: false },
59
+ html: { type: "boolean", required: false },
60
+ },
61
+ async execute(input, ctx) {
62
+ const r = await graphRequest(ctx, NAME, SCOPES, "POST", `${userBase(ctx)}/messages`, toMessage(input));
63
+ return { id: r.id, webLink: r.webLink };
64
+ },
65
+ });
66
+ rl.registerAction("mail.list", {
67
+ description: "List recent messages. Optional KQL search. Returns [{id,subject,from,receivedDateTime,bodyPreview,hasAttachments}].",
68
+ inputSchema: {
69
+ search: { type: "string", required: false, description: "KQL search across the mailbox" },
70
+ top: { type: "number", required: false, default: 20 },
71
+ },
72
+ async execute(input, ctx) {
73
+ const qs = new URLSearchParams({
74
+ $top: String(input.top ?? 20),
75
+ $select: "id,subject,from,receivedDateTime,bodyPreview,hasAttachments",
76
+ $orderby: "receivedDateTime desc",
77
+ });
78
+ if (input.search)
79
+ qs.set("$search", `"${input.search}"`);
80
+ const r = await graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/messages?${qs}`);
81
+ return r.value;
82
+ },
83
+ });
84
+ rl.registerAction("mail.get", {
85
+ description: "Get one message with full body by id.",
86
+ inputSchema: { id: { type: "string", required: true } },
87
+ async execute(input, ctx) {
88
+ return graphRequest(ctx, NAME, SCOPES, "GET", `${userBase(ctx)}/messages/${input.id}`);
89
+ },
90
+ });
91
+ }
@@ -1,23 +1,22 @@
1
1
  /**
2
2
  * OpenAI image generation for runline.
3
3
  *
4
- * Wraps the GPT Image / DALL-E line at /v1/images/generations and
5
- * returns base64 bytes alongside the (optional) revised prompt the
6
- * model wrote for itself.
4
+ * Wraps the GPT Image / DALL-E line at /v1/images/generations. Generated
5
+ * images are written to disk and the action returns their file `path`s
6
+ * (plus the optional revised prompt) — never raw base64, which bloats the
7
+ * agent context and is stripped before delivery. Hand each `path` to the
8
+ * host's file-sending tool (e.g. send_file) to deliver the image.
7
9
  *
8
10
  * Quality leader for text rendering and prompt adherence — pair with
9
11
  * any other plugin you'd compose images for (storyblok, github,
10
12
  * notion, slack uploads, …).
11
13
  *
12
- * await openai.image.create({ prompt: "a red bicycle on snow" })
13
- * await openai.image.create({
14
- * prompt: "logo for a coffee shop",
15
- * model: "dall-e-3",
16
- * style: "vivid",
17
- * quality: "high",
18
- * size: "1024x1024",
19
- * })
14
+ * const { images } = await openai.image.create({ prompt: "a red bicycle on snow" })
15
+ * // images[0].path -> "/tmp/openai-image-….png"
20
16
  */
17
+ import { writeFileSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
21
20
  const ENDPOINT = "https://api.openai.com/v1/images/generations";
22
21
  export default function openai(rl) {
23
22
  rl.setName("openai");
@@ -29,19 +28,30 @@ export default function openai(rl) {
29
28
  description: "OpenAI API key",
30
29
  env: "OPENAI_API_KEY",
31
30
  },
31
+ defaultModel: {
32
+ type: "string",
33
+ required: false,
34
+ description: "Default image model when a call omits `model` (e.g. gpt-image-2). Falls back to gpt-image-1.",
35
+ env: "OPENAI_IMAGE_MODEL",
36
+ },
32
37
  });
33
38
  rl.registerAction("image.create", {
34
- description: "Generate an image with OpenAI (GPT Image / DALL-E). Returns base64-encoded PNGs and any revised prompt the model produced.",
39
+ description: "Generate an image with OpenAI (GPT Image / DALL-E). Writes the PNG(s) to disk and returns their file `path`s (plus any revised prompt) — not base64. Deliver each image to the user with send_file using its `path`.",
35
40
  inputSchema: {
36
41
  prompt: {
37
42
  type: "string",
38
43
  required: true,
39
44
  description: "Detailed description of the image",
40
45
  },
46
+ saveDir: {
47
+ type: "string",
48
+ required: false,
49
+ description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
50
+ },
41
51
  model: {
42
52
  type: "string",
43
53
  required: false,
44
- description: "gpt-image-1 (default) | gpt-image-1-mini | dall-e-3 | dall-e-2",
54
+ description: "gpt-image-2 | gpt-image-1 | gpt-image-1-mini | dall-e-3 | dall-e-2. Omit to use the connection default.",
45
55
  },
46
56
  size: {
47
57
  type: "string",
@@ -70,7 +80,9 @@ export default function openai(rl) {
70
80
  throw new Error("openai: prompt is required");
71
81
  }
72
82
  const apiKey = ctx.connection.config.apiKey;
73
- const model = p.model ?? "gpt-image-1";
83
+ const model = p.model ??
84
+ ctx.connection.config.defaultModel ??
85
+ "gpt-image-1";
74
86
  const body = {
75
87
  model,
76
88
  prompt: p.prompt,
@@ -101,12 +113,25 @@ export default function openai(rl) {
101
113
  throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
102
114
  }
103
115
  const data = (await res.json());
104
- const images = (data.data ?? []).map((d) => ({
105
- base64: d.b64_json,
106
- mimeType: "image/png",
107
- ...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
108
- }));
109
- return { provider: "openai", model, images };
116
+ const dir = (typeof p.saveDir === "string" && p.saveDir.trim()) || tmpdir();
117
+ const stamp = Date.now();
118
+ const images = (data.data ?? []).map((d, i) => {
119
+ const bytes = Buffer.from(d.b64_json ?? "", "base64");
120
+ const path = join(dir, `openai-image-${stamp}-${i}.png`);
121
+ writeFileSync(path, bytes);
122
+ return {
123
+ path,
124
+ mimeType: "image/png",
125
+ byteLength: bytes.length,
126
+ ...(d.revised_prompt ? { revisedPrompt: d.revised_prompt } : {}),
127
+ };
128
+ });
129
+ return {
130
+ provider: "openai",
131
+ model,
132
+ images,
133
+ note: "Image(s) written to disk. Deliver each to the user with send_file using its `path`.",
134
+ };
110
135
  },
111
136
  });
112
137
  }
@@ -0,0 +1,100 @@
1
+ const NAME = "parallel";
2
+ const DEFAULT_BASE = "https://api.parallel.ai";
3
+ export default function parallel(rl) {
4
+ rl.setName(NAME);
5
+ rl.setVersion("0.1.0");
6
+ rl.setConnectionSchema({
7
+ apiKey: {
8
+ type: "string",
9
+ required: true,
10
+ env: "PARALLEL_API_KEY",
11
+ description: "Parallel.ai API key (sent as the x-api-key header). Store only in secrets.",
12
+ },
13
+ baseUrl: {
14
+ type: "string",
15
+ required: false,
16
+ env: "PARALLEL_API_BASE",
17
+ default: DEFAULT_BASE,
18
+ description: "Parallel.ai API base URL.",
19
+ },
20
+ });
21
+ rl.registerAction("search", {
22
+ description: "Search the live web with Parallel.ai and get ranked results with extracted excerpts. Use this to ground answers in current web content (news, prices, regulations, company info) instead of stale knowledge. Provide an `objective` (natural-language goal) and/or explicit `search_queries`.",
23
+ inputSchema: {
24
+ objective: {
25
+ type: "string",
26
+ required: false,
27
+ description: "Natural-language description of what you're trying to find. Recommended; can be used with or instead of search_queries.",
28
+ },
29
+ search_queries: {
30
+ type: "array",
31
+ required: false,
32
+ description: "Optional explicit query strings to run (e.g. [\"x vacancy rate 2026\"]). Provide objective and/or this.",
33
+ },
34
+ processor: {
35
+ type: "string",
36
+ required: false,
37
+ default: "base",
38
+ description: "base (fast, default) or pro (deeper, slower/costlier).",
39
+ },
40
+ max_results: {
41
+ type: "number",
42
+ required: false,
43
+ default: 5,
44
+ description: "Max results to return (default 5).",
45
+ },
46
+ max_chars_per_result: {
47
+ type: "number",
48
+ required: false,
49
+ description: "Optional cap on extracted characters per result.",
50
+ },
51
+ },
52
+ async execute(input, ctx) {
53
+ const cfg = (ctx.connection.config ?? {});
54
+ const apiKey = cfg.apiKey;
55
+ if (!apiKey)
56
+ throw new Error("Missing PARALLEL_API_KEY. Configure it before using the parallel plugin.");
57
+ const baseUrl = (cfg.baseUrl || DEFAULT_BASE).replace(/\/+$/, "");
58
+ const objective = typeof input.objective === "string" ? input.objective.trim() : "";
59
+ const queries = Array.isArray(input.search_queries)
60
+ ? input.search_queries.map((q) => String(q)).filter((q) => q.trim())
61
+ : [];
62
+ if (!objective && !queries.length) {
63
+ throw new Error("Provide objective and/or search_queries");
64
+ }
65
+ const body = {
66
+ processor: String(input.processor || "base"),
67
+ max_results: Number(input.max_results) > 0 ? Math.floor(Number(input.max_results)) : 5,
68
+ };
69
+ if (objective)
70
+ body.objective = objective;
71
+ if (queries.length)
72
+ body.search_queries = queries;
73
+ if (Number(input.max_chars_per_result) > 0) {
74
+ body.max_chars_per_result = Math.floor(Number(input.max_chars_per_result));
75
+ }
76
+ const res = await fetch(`${baseUrl}/v1beta/search`, {
77
+ method: "POST",
78
+ headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
79
+ body: JSON.stringify(body),
80
+ });
81
+ const text = await res.text();
82
+ if (!res.ok) {
83
+ throw new Error(`Parallel search -> ${res.status}: ${text.slice(0, 300)}`);
84
+ }
85
+ const data = text ? JSON.parse(text) : {};
86
+ const results = (data.results ?? []).map((r) => ({
87
+ url: r.url,
88
+ title: r.title,
89
+ excerpts: r.excerpts ?? [],
90
+ }));
91
+ return {
92
+ searchId: data.search_id,
93
+ count: results.length,
94
+ results,
95
+ warnings: data.warnings ?? undefined,
96
+ source: "parallel.search",
97
+ };
98
+ },
99
+ });
100
+ }
@@ -13,6 +13,7 @@
13
13
  * legacy `style` knob — pass `styleId` against your own custom
14
14
  * style if you've set one up.
15
15
  */
16
+ import { SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
16
17
  const ENDPOINT = "https://external.api.recraft.ai/v1/images/generations";
17
18
  export default function recraft(rl) {
18
19
  rl.setName("recraft");
@@ -26,13 +27,18 @@ export default function recraft(rl) {
26
27
  },
27
28
  });
28
29
  rl.registerAction("image.create", {
29
- description: "Generate an image with Recraft. Best for design, vector graphics, and brand-consistent work. Returns base64-encoded PNGs.",
30
+ description: "Generate an image with Recraft. Best for design, vector graphics, and brand-consistent work. Writes the image(s) to disk and returns their file `path`s — not base64. Deliver each with send_file using its `path`.",
30
31
  inputSchema: {
31
32
  prompt: {
32
33
  type: "string",
33
34
  required: true,
34
35
  description: "Detailed description of the image",
35
36
  },
37
+ saveDir: {
38
+ type: "string",
39
+ required: false,
40
+ description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
41
+ },
36
42
  model: {
37
43
  type: "string",
38
44
  required: false,
@@ -90,11 +96,9 @@ export default function recraft(rl) {
90
96
  throw new Error(`Recraft API error ${res.status}: ${await res.text()}`);
91
97
  }
92
98
  const data = (await res.json());
93
- const images = (data.data ?? []).map((d) => ({
94
- base64: d.b64_json,
95
- mimeType: "image/png",
96
- }));
97
- return { provider: "recraft", model, images };
99
+ const stamp = Date.now();
100
+ const images = (data.data ?? []).map((d, i) => writeImageFile({ base64: d.b64_json, mimeType: "image/png", provider: "recraft", index: i, saveDir: p.saveDir, stamp }));
101
+ return { provider: "recraft", model, images, note: SEND_FILE_NOTE };
98
102
  },
99
103
  });
100
104
  }
@@ -19,6 +19,7 @@
19
19
  * separately.
20
20
  */
21
21
  import { Buffer } from "node:buffer";
22
+ import { SEND_FILE_NOTE, writeImageFile } from "../../_shared/imageFile.js";
22
23
  import { parseSize } from "../../_shared/parseSize.js";
23
24
  const POLL_INTERVAL_MS = 2_000;
24
25
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
@@ -46,13 +47,18 @@ export default function replicate(rl) {
46
47
  },
47
48
  });
48
49
  rl.registerAction("image.create", {
49
- description: "Generate an image via Replicate. Default model is black-forest-labs/flux-dev. Returns base64 bytes from the model's output URLs.",
50
+ description: "Generate an image via Replicate. Default model is black-forest-labs/flux-dev. Writes the image(s) to disk and returns their file `path`s not base64. Deliver each with send_file using its `path`.",
50
51
  inputSchema: {
51
52
  prompt: {
52
53
  type: "string",
53
54
  required: true,
54
55
  description: "Detailed description of the image",
55
56
  },
57
+ saveDir: {
58
+ type: "string",
59
+ required: false,
60
+ description: "Directory to write the image file(s) into. Defaults to the OS temp dir.",
61
+ },
56
62
  model: {
57
63
  type: "string",
58
64
  required: false,
@@ -136,6 +142,7 @@ export default function replicate(rl) {
136
142
  : [];
137
143
  const images = [];
138
144
  const failures = [];
145
+ const stamp = Date.now();
139
146
  for (const url of outputs) {
140
147
  if (typeof url !== "string") {
141
148
  failures.push({ url: String(url), reason: "non-string output" });
@@ -153,13 +160,20 @@ export default function replicate(rl) {
153
160
  const contentType = (imgRes.headers.get("content-type") ?? "image/webp")
154
161
  .split(";")[0]
155
162
  .trim();
156
- images.push({ base64: buf.toString("base64"), mimeType: contentType });
163
+ images.push(writeImageFile({
164
+ base64: buf.toString("base64"),
165
+ mimeType: contentType,
166
+ provider: "replicate",
167
+ index: images.length,
168
+ saveDir: p.saveDir,
169
+ stamp,
170
+ }));
157
171
  }
158
172
  if (images.length === 0 && outputs.length > 0) {
159
173
  const detail = failures.map((f) => `${f.url}: ${f.reason}`).join("; ");
160
174
  throw new Error(`Replicate succeeded but all ${outputs.length} output URLs failed to download — ${detail}`);
161
175
  }
162
- const result = { provider: "replicate", model, images };
176
+ const result = { provider: "replicate", model, images, note: SEND_FILE_NOTE };
163
177
  if (failures.length > 0)
164
178
  result.failures = failures;
165
179
  return result;