Skip to content

Turn Resend into an AI Extension#17444

Merged
raycastbot merged 13 commits intomainfrom
@peduarte/resend
Mar 5, 2025
Merged

Turn Resend into an AI Extension#17444
raycastbot merged 13 commits intomainfrom
@peduarte/resend

Conversation

@peduarte
Copy link
Contributor

@peduarte peduarte commented Feb 28, 2025

Description

Added a few AI Tools so users can interact with this extension via natural language.

Screencast

(ಠ ›ಠ) — @peduarte — lNd23hcR@2x

Checklist

@raycastbot raycastbot added extension fix / improvement Label for PRs with extension's fix improvements extension: resend Issues related to the resend extension labels Feb 28, 2025
@raycastbot
Copy link
Collaborator

raycastbot commented Feb 28, 2025

Thank you for your contribution! 🎉

🔔 @xmok @AlexIsMaking you might want to have a look.

You can use this guide to learn how to check out the Pull Request locally in order to test it.

Due to our current reduced availability, the initial review may take up to 10-15 business days

@peduarte peduarte marked this pull request as draft February 28, 2025 17:32
@pernielsentikaer pernielsentikaer self-assigned this Mar 3, 2025
@pernielsentikaer
Copy link
Collaborator

@greptileai can you check this 😊

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This PR transforms the Resend extension into an AI-enabled extension, adding natural language interaction capabilities through 11 AI tools for email management, contacts, audiences, and API keys.

  • Added comprehensive AI tools in /extensions/resend/src/tools/ for sending emails, managing contacts, handling API keys, and more with proper confirmation dialogs
  • Added new required preferences in package.json for sender information (sender_name and sender_email) used as defaults when sending emails
  • Added proper AI evals in package.json with example inputs for testing the extension's AI capabilities
  • Updated README.md and CHANGELOG.md to document the new AI functionality and supported operations
  • Modified constants.ts to export the API key for use across tool implementations, ensuring consistent authentication

💡 (2/5) Greptile learns from your feedback when you react with 👍/👎!

16 file(s) reviewed, 11 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +110 to +115
const tags = input.tags
? input.tags.split("\n").map((line) => {
const [name, value] = line.split(":");
return { name, value };
})
: undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The tag parsing doesn't handle the case where a tag line doesn't contain a colon. This could lead to undefined values in the tags array.

@peduarte peduarte marked this pull request as ready for review March 3, 2025 12:38
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

(updates since last review)

This PR enhances the Resend extension with AI tools for natural language interaction, focusing on email management and API functionality.

  • Added proper confirmation dialogs with message properties in create-contact.ts and create-api-key.ts to provide context before actions
  • Modified send-email.ts to change confirmation dialog title from "Send Email" to "Send email" (lowercase 'e')
  • All tools now include proper JSDoc comments to guide AI in understanding required parameters and their usage
  • Tools implementation follows a consistent pattern with proper error handling and type definitions

3 file(s) reviewed, no comment(s)
Edit PR Review Bot Settings | Greptile

Copy link
Contributor

@xmok xmok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool! Looking forward to this one ngl.

I would like the default Preferences to be optional as not all might want to use default settings (myself included). But this is a non-blocker for me we can leave it as-is.

Can we get the previous PR merged? The comment I added we can disregard I'll address it myself in a future PR.

After that LGTM 👀.

@pernielsentikaer
Copy link
Collaborator

pernielsentikaer commented Mar 5, 2025

I'm getting this error @peduarte when I try to use the extension, do you have any idea?

Raycast 2025-03-05 at 08 12 39

07:11:17 
ReferenceError: Headers is not defined

Resend:index.mjs:470:23

---
467:         );
468:       }
469:     }
470:     this.headers = new Headers({
471:       Authorization: `Bearer ${this.key}`,
472:       "User-Agent": userAgent,
473:       "Content-Type": "application/json"
---

Object.<anonymous>:send-email.js:32345:14
Module._compile:loader:1358:14
Object..js:loader:1416:10
{
  "message" : "Headers is not defined",
  "sessionId" : "7B8F5AF2-C070-47B0-96DA-FF0D03CCB018",
  "title" : "Worker Request Error",
  "stack" : [
    {
      "raw" : "    at new Resend (/Users/pernielsentikaer/.config/raycast/extensions/resend/tools/send-email.js:32235:24)",
      "line" : 470,
      "file" : "/Users/pernielsentikaer/Documents/Raycast/tmp/resend/node_modules/resend/dist/index.mjs",
      "fileContent" : "var __defProp = Object.defineProperty;\nvar __defProps = Object.defineProperties;\nvar __getOwnPropDescs = Object.getOwnPropertyDescriptors;\nvar __getOwnPropSymbols = Object.getOwnPropertySymbols;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __propIsEnum = Object.prototype.propertyIsEnumerable;\nvar __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;\nvar __spreadValues = (a, b) => {\n  for (var prop in b || (b = {}))\n    if (__hasOwnProp.call(b, prop))\n      __defNormalProp(a, prop, b[prop]);\n  if (__getOwnPropSymbols)\n    for (var prop of __getOwnPropSymbols(b)) {\n      if (__propIsEnum.call(b, prop))\n        __defNormalProp(a, prop, b[prop]);\n    }\n  return a;\n};\nvar __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));\nvar __async = (__this, __arguments, generator) => {\n  return new Promise((resolve, reject) => {\n    var fulfilled = (value) => {\n      try {\n        step(generator.next(value));\n      } catch (e) {\n        reject(e);\n      }\n    };\n    var rejected = (value) => {\n      try {\n        step(generator.throw(value));\n      } catch (e) {\n        reject(e);\n      }\n    };\n    var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);\n    step((generator = generator.apply(__this, __arguments)).next());\n  });\n};\n\n// package.json\nvar version = \"4.1.2\";\n\n// src/api-keys/api-keys.ts\nvar ApiKeys = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      const data = yield this.resend.post(\n        \"/api-keys\",\n        payload,\n        options\n      );\n      return data;\n    });\n  }\n  list() {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\"/api-keys\");\n      return data;\n    });\n  }\n  remove(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.delete(\n        `/api-keys/${id}`\n      );\n      return data;\n    });\n  }\n};\n\n// src/audiences/audiences.ts\nvar Audiences = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      const data = yield this.resend.post(\n        \"/audiences\",\n        payload,\n        options\n      );\n      return data;\n    });\n  }\n  list() {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\"/audiences\");\n      return data;\n    });\n  }\n  get(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\n        `/audiences/${id}`\n      );\n      return data;\n    });\n  }\n  remove(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.delete(\n        `/audiences/${id}`\n      );\n      return data;\n    });\n  }\n};\n\n// src/common/utils/parse-email-to-api-options.ts\nfunction parseEmailToApiOptions(email) {\n  return {\n    attachments: email.attachments,\n    bcc: email.bcc,\n    cc: email.cc,\n    from: email.from,\n    headers: email.headers,\n    html: email.html,\n    reply_to: email.replyTo,\n    scheduled_at: email.scheduledAt,\n    subject: email.subject,\n    tags: email.tags,\n    text: email.text,\n    to: email.to\n  };\n}\n\n// src/batch/batch.ts\nvar Batch = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  send(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      return this.create(payload, options);\n    });\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      const emails = [];\n      for (const email of payload) {\n        if (email.react) {\n          if (!this.renderAsync) {\n            try {\n              const { renderAsync } = yield import(\"@react-email/render\");\n              this.renderAsync = renderAsync;\n            } catch (error) {\n              throw new Error(\n                \"Failed to render React component. Make sure to install `@react-email/render`\"\n              );\n            }\n          }\n          email.html = yield this.renderAsync(email.react);\n          email.react = void 0;\n        }\n        emails.push(parseEmailToApiOptions(email));\n      }\n      const data = yield this.resend.post(\n        \"/emails/batch\",\n        emails,\n        options\n      );\n      return data;\n    });\n  }\n};\n\n// src/broadcasts/broadcasts.ts\nvar Broadcasts = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      if (payload.react) {\n        if (!this.renderAsync) {\n          try {\n            const { renderAsync } = yield import(\"@react-email/render\");\n            this.renderAsync = renderAsync;\n          } catch (error) {\n            throw new Error(\n              \"Failed to render React component. Make sure to install `@react-email/render`\"\n            );\n          }\n        }\n        payload.html = yield this.renderAsync(\n          payload.react\n        );\n      }\n      const data = yield this.resend.post(\n        \"/broadcasts\",\n        {\n          name: payload.name,\n          audience_id: payload.audienceId,\n          preview_text: payload.previewText,\n          from: payload.from,\n          html: payload.html,\n          reply_to: payload.replyTo,\n          subject: payload.subject,\n          text: payload.text\n        },\n        options\n      );\n      return data;\n    });\n  }\n  send(id, payload) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.post(\n        `/broadcasts/${id}/send`,\n        { scheduled_at: payload == null ? void 0 : payload.scheduledAt }\n      );\n      return data;\n    });\n  }\n  list() {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\"/broadcasts\");\n      return data;\n    });\n  }\n  get(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\n        `/broadcasts/${id}`\n      );\n      return data;\n    });\n  }\n  remove(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.delete(\n        `/broadcasts/${id}`\n      );\n      return data;\n    });\n  }\n};\n\n// src/contacts/contacts.ts\nvar Contacts = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      const data = yield this.resend.post(\n        `/audiences/${payload.audienceId}/contacts`,\n        {\n          unsubscribed: payload.unsubscribed,\n          email: payload.email,\n          first_name: payload.firstName,\n          last_name: payload.lastName\n        },\n        options\n      );\n      return data;\n    });\n  }\n  list(options) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\n        `/audiences/${options.audienceId}/contacts`\n      );\n      return data;\n    });\n  }\n  get(options) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\n        `/audiences/${options.audienceId}/contacts/${options.id}`\n      );\n      return data;\n    });\n  }\n  update(payload) {\n    return __async(this, null, function* () {\n      if (!payload.id && !payload.email) {\n        return {\n          data: null,\n          error: {\n            message: \"Missing `id` or `email` field.\",\n            name: \"missing_required_field\"\n          }\n        };\n      }\n      const data = yield this.resend.patch(\n        `/audiences/${payload.audienceId}/contacts/${(payload == null ? void 0 : payload.email) ? payload == null ? void 0 : payload.email : payload == null ? void 0 : payload.id}`,\n        {\n          unsubscribed: payload.unsubscribed,\n          first_name: payload.firstName,\n          last_name: payload.lastName\n        }\n      );\n      return data;\n    });\n  }\n  remove(payload) {\n    return __async(this, null, function* () {\n      if (!payload.id && !payload.email) {\n        return {\n          data: null,\n          error: {\n            message: \"Missing `id` or `email` field.\",\n            name: \"missing_required_field\"\n          }\n        };\n      }\n      const data = yield this.resend.delete(\n        `/audiences/${payload.audienceId}/contacts/${(payload == null ? void 0 : payload.email) ? payload == null ? void 0 : payload.email : payload == null ? void 0 : payload.id}`\n      );\n      return data;\n    });\n  }\n};\n\n// src/domains/domains.ts\nvar Domains = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      const data = yield this.resend.post(\n        \"/domains\",\n        payload,\n        options\n      );\n      return data;\n    });\n  }\n  list() {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\"/domains\");\n      return data;\n    });\n  }\n  get(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\n        `/domains/${id}`\n      );\n      return data;\n    });\n  }\n  update(payload) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.patch(\n        `/domains/${payload.id}`,\n        {\n          click_tracking: payload.clickTracking,\n          open_tracking: payload.openTracking,\n          tls: payload.tls\n        }\n      );\n      return data;\n    });\n  }\n  remove(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.delete(\n        `/domains/${id}`\n      );\n      return data;\n    });\n  }\n  verify(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.post(\n        `/domains/${id}/verify`\n      );\n      return data;\n    });\n  }\n};\n\n// src/emails/emails.ts\nvar Emails = class {\n  constructor(resend) {\n    this.resend = resend;\n  }\n  send(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      return this.create(payload, options);\n    });\n  }\n  create(_0) {\n    return __async(this, arguments, function* (payload, options = {}) {\n      if (payload.react) {\n        if (!this.renderAsync) {\n          try {\n            const { renderAsync } = yield import(\"@react-email/render\");\n            this.renderAsync = renderAsync;\n          } catch (error) {\n            throw new Error(\n              \"Failed to render React component. Make sure to install `@react-email/render`\"\n            );\n          }\n        }\n        payload.html = yield this.renderAsync(\n          payload.react\n        );\n      }\n      const data = yield this.resend.post(\n        \"/emails\",\n        parseEmailToApiOptions(payload),\n        options\n      );\n      return data;\n    });\n  }\n  get(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.get(\n        `/emails/${id}`\n      );\n      return data;\n    });\n  }\n  update(payload) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.patch(\n        `/emails/${payload.id}`,\n        {\n          scheduled_at: payload.scheduledAt\n        }\n      );\n      return data;\n    });\n  }\n  cancel(id) {\n    return __async(this, null, function* () {\n      const data = yield this.resend.post(\n        `/emails/${id}/cancel`\n      );\n      return data;\n    });\n  }\n};\n\n// src/resend.ts\nvar defaultBaseUrl = \"https://api.resend.com\";\nvar defaultUserAgent = `resend-node:${version}`;\nvar baseUrl = typeof process !== \"undefined\" && process.env ? process.env.RESEND_BASE_URL || defaultBaseUrl : defaultBaseUrl;\nvar userAgent = typeof process !== \"undefined\" && process.env ? process.env.RESEND_USER_AGENT || defaultUserAgent : defaultUserAgent;\nvar Resend = class {\n  constructor(key) {\n    this.key = key;\n    this.apiKeys = new ApiKeys(this);\n    this.audiences = new Audiences(this);\n    this.batch = new Batch(this);\n    this.broadcasts = new Broadcasts(this);\n    this.contacts = new Contacts(this);\n    this.domains = new Domains(this);\n    this.emails = new Emails(this);\n    if (!key) {\n      if (typeof process !== \"undefined\" && process.env) {\n        this.key = process.env.RESEND_API_KEY;\n      }\n      if (!this.key) {\n        throw new Error(\n          'Missing API key. Pass it to the constructor `new Resend(\"re_123\")`'\n        );\n      }\n    }\n    this.headers = new Headers({\n      Authorization: `Bearer ${this.key}`,\n      \"User-Agent\": userAgent,\n      \"Content-Type\": \"application/json\"\n    });\n  }\n  fetchRequest(_0) {\n    return __async(this, arguments, function* (path, options = {}) {\n      try {\n        const response = yield fetch(`${baseUrl}${path}`, options);\n        if (!response.ok) {\n          try {\n            const rawError = yield response.text();\n            return { data: null, error: JSON.parse(rawError) };\n          } catch (err) {\n            if (err instanceof SyntaxError) {\n              return {\n                data: null,\n                error: {\n                  name: \"application_error\",\n                  message: \"Internal server error. We are unable to process your request right now, please try again later.\"\n                }\n              };\n            }\n            const error = {\n              message: response.statusText,\n              name: \"application_error\"\n            };\n            if (err instanceof Error) {\n              return { data: null, error: __spreadProps(__spreadValues({}, error), { message: err.message }) };\n            }\n            return { data: null, error };\n          }\n        }\n        const data = yield response.json();\n        return { data, error: null };\n      } catch (error) {\n        return {\n          data: null,\n          error: {\n            name: \"application_error\",\n            message: \"Unable to fetch data. The request could not be resolved.\"\n          }\n        };\n      }\n    });\n  }\n  post(_0, _1) {\n    return __async(this, arguments, function* (path, entity, options = {}) {\n      const requestOptions = __spreadValues({\n        method: \"POST\",\n        headers: this.headers,\n        body: JSON.stringify(entity)\n      }, options);\n      return this.fetchRequest(path, requestOptions);\n    });\n  }\n  get(_0) {\n    return __async(this, arguments, function* (path, options = {}) {\n      const requestOptions = __spreadValues({\n        method: \"GET\",\n        headers: this.headers\n      }, options);\n      return this.fetchRequest(path, requestOptions);\n    });\n  }\n  put(_0, _1) {\n    return __async(this, arguments, function* (path, entity, options = {}) {\n      const requestOptions = __spreadValues({\n        method: \"PUT\",\n        headers: this.headers,\n        body: JSON.stringify(entity)\n      }, options);\n      return this.fetchRequest(path, requestOptions);\n    });\n  }\n  patch(_0, _1) {\n    return __async(this, arguments, function* (path, entity, options = {}) {\n      const requestOptions = __spreadValues({\n        method: \"PATCH\",\n        headers: this.headers,\n        body: JSON.stringify(entity)\n      }, options);\n      return this.fetchRequest(path, requestOptions);\n    });\n  }\n  delete(path, query) {\n    return __async(this, null, function* () {\n      const requestOptions = {\n        method: \"DELETE\",\n        headers: this.headers,\n        body: JSON.stringify(query)\n      };\n      return this.fetchRequest(path, requestOptions);\n    });\n  }\n};\nexport {\n  Resend\n};\n",
      "function" : "Resend",
      "column" : 23
    },
    {
      "raw" : "    at Object.<anonymous> (/Users/pernielsentikaer/.config/raycast/extensions/resend/tools/send-email.js:32345:14)",
      "line" : 32345,
      "file" : "/Users/pernielsentikaer/.config/raycast/extensions/resend/tools/send-email.js",
      "function" : "Object.<anonymous>",
      "column" : 14
    },
    {
      "raw" : "    at Module._compile (node:internal/modules/cjs/loader:1358:14)",
      "line" : 1358,
      "file" : "node:internal/modules/cjs/loader",
      "function" : "Module._compile",
      "column" : 14
    },
    {
      "raw" : "    at Object..js (node:internal/modules/cjs/loader:1416:10)",
      "line" : 1416,
      "file" : "node:internal/modules/cjs/loader",
      "function" : "Object..js",
      "column" : 10
    }
  ],
  "name" : "ReferenceError",
  "diagnostics" : {
    "mode" : "no-view",
    "extensionName" : "resend",
    "name" : "tools/send-email",
    "launchType" : "userInitiated",
    "startedAt" : 1741158677.271237,
    "isDevelopment" : true,
    "timeout" : 600,
    "id" : "7B8F5AF2-C070-47B0-96DA-FF0D03CCB018",
    "eventLoopUtilization" : {
      "utilization" : 1,
      "idle" : 0,
      "active" : 0.004541993141174316
    }
  },
  "code" : 8
}

@raycastbot raycastbot merged commit 7487af0 into main Mar 5, 2025
9 checks passed
@raycastbot raycastbot deleted the @peduarte/resend branch March 5, 2025 12:21
@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2025

Published to the Raycast Store:
https://raycast.com/resend/resend

@raycastbot
Copy link
Collaborator

🎉 🎉 🎉

We've rewarded your Raycast account with some credits. You will soon be able to exchange them for some swag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extension fix / improvement Label for PRs with extension's fix improvements extension: resend Issues related to the resend extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants