Shortcut to Everything

An open-source kit to optimize your developer workflow

Download Script Kit:

microsoft Windows
linux Linux (Community)
John Lindquist

Script Kit is made for developers who understand the inherent risks of running scripts on their computer. If you're uncomfortable with any aspect of writing, running, or sharing scripts, please ask for help!

Script Kit doesn't collect any personal information.


What is Script Kit?

How often do you avoid scripting something because it takes too much effort?

Script Kit makes it easy to create and run scripts that solve your daily problems. Create a new script from the prompt then your script opens in the editor of your choice. Write a few lines of JavaScript. Then run the script from the prompt.

Simply put, Script Kit helps you script away the friction of your day.

Key Features

  • ▪︎Write your scripts in TypeScript with our SDK. Zero config.
  • ▪︎Launch the prompt from anywhere as part of your flow
  • ▪︎Add keyboard shortcuts with comments://Shortcut: opt a
  • ▪︎Prompt for input with:await arg("First name")
  • ▪︎Prompt for environment vars:await env("GITHUB_TOKEN")
  • ▪︎Watches your scripts and prompts to install npm libraries:import express from "express"
  • ▪︎Share scripts directly from the prompt
  • ▪︎Launch scripts from a Stream Deck button
  • ▪︎Scripts behave the same in your terminal

Learn more

Script Kit includes a built-in tutorial and myriad examples are available on our discussions pages:

Join the Script Kit community and stay up-to-date on the latest updates!

No spam. We respect your privacy. Unsubscribe at any time.

What the community is saying

I forgot that a lot of people don't know what @scriptkitapp is. You're missing out! I use it to easily open projects in VSCode, start a zoom meeting and put the link in my clipboard, download Twitter images, upload images to cloudinary, and so much more!

So, @scriptkitapp is AMAZING.

Just spent a very happy morning figuring out a script where it takes my latest recording from OBS and trims the silence from the start and end with ffmpeg.

Now, it's just a command palette action away.

I just learned about Script Kit, and my mind is racing with all the stuff I could do with it.

@scriptkitapp is dope!

Took me less then a minute to create a command palette script for killing a port 🤩

Featured Scripts

// Menu: New Post
// Description: Create a new blog post
// Author: Kent C. Dodds
// Shortcut: command option control p
// Twitter: @kentcdodds
const dateFns = await npm('date-fns')
const prettier = await npm('prettier')
const YAML = await npm('yaml')
const slugify = await npm('@sindresorhus/slugify')
const {format: formatDate} = await npm('date-fns')
const makeMetascraper = await npm('metascraper')
const {$filter, toRule} = await npm('@metascraper/helpers')
const unsplashTitleToAlt = toRule(str => str.replace(/ photo – .*$/, ''))
const unsplashOGTitleToAuthor = toRule(str =>
str.replace(/Photo by (.*?) on Unsplash/, '$1'),
)
const unsplashImageToPhotoId = toRule(str =>
new URL(str).pathname.replace('/', ''),
)
const metascraper = makeMetascraper([
{
unsplashPhotoId: [
unsplashImageToPhotoId($ =>
$('meta[property="og:image"]').attr('content'),
),
],
},
{
author: [
unsplashOGTitleToAuthor($ =>
$('meta[property="og:title"]').attr('content'),
),
],
},
{alt: [unsplashTitleToAlt($ => $('title').text())]},
])
async function getMetadata(url) {
const html = await fetch(url).then(res => res.text())
return metascraper({html, url})
}
const blogDir = await env(
'KCD_BLOG_CONTENT_DIR',
`What's the path to the blog content directory on this machine?`,
)
const title = await arg({
placeholder: `What's the title of this post?`,
hint: 'Title',
ignoreBlur: true,
})
const description = await arg({
placeholder: `What's the description of this post?`,
hint: 'Description',
input: 'TODO: add a description',
ignoreBlur: true,
})
const categories = (
await arg({
placeholder: `What are the categories of this post?`,
hint: 'Categories (comma separated)',
ignoreBlur: true,
})
)
.split(',')
.map(c => c.trim())
.filter(Boolean)
const keywords = (
await arg({
placeholder: `What are the keywords of this post?`,
hint: 'Keywords (comma separated)',
ignoreBlur: true,
})
)
.split(',')
.map(c => c.trim())
.filter(Boolean)
const filename = slugify(title, {decamelize: false})
await exec(`open https://unsplash.com/s/photos/${filename}`)
const unsplashPhotoInput = await arg({
placeholder: `What's the unsplash photo?`,
hint: 'Unsplash Photo',
ignoreBlur: true,
})
const unsplashPhotoUrl = unsplashPhotoInput.startsWith('http')
? unsplashPhotoInput
: `https://unsplash.com/photos/${unsplashPhotoInput}`
const metadata = await getMetadata(unsplashPhotoUrl)
const frontmatter = YAML.stringify({
title,
date: dateFns.format(new Date(), 'yyyy-MM-dd'),
description,
categories,
meta: {keywords},
bannerCloudinaryId: `unsplash/${metadata.unsplashPhotoId}`,
bannerAlt: metadata.alt,
bannerCredit: `Photo by [${metadata.author}](${unsplashPhotoUrl})`,
})
const md = `---
${frontmatter}
---
Be excellent to each other.
`
// prettify the markdown
const prettyMd = await prettier.format(md, {
parser: 'markdown',
arrowParens: 'avoid',
bracketSpacing: false,
embeddedLanguageFormatting: 'auto',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxBracketSameLine: false,
jsxSingleQuote: false,
printWidth: 80,
proseWrap: 'always',
quoteProps: 'as-needed',
requirePragma: false,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
vueIndentScriptAndStyle: false,
})
const newFile = path.join(blogDir, `${filename}.mdx`)
await writeFile(newFile, prettyMd)
await edit(newFile)
// Menu: Giphy
// Description: Search giphy. Paste link.
// Author: John Lindquist
// Twitter: @johnlindquist
let download = await npm("image-downloader")
let queryString = await npm("query-string")
let GIPHY_API_KEY = await env("GIPHY_API_KEY", {
hint: md(
`Get a [Giphy API Key](https://developers.giphy.com/dashboard/)`
),
ignoreBlur: true,
secret: true,
})
let search = q =>
`https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&q=${q}&limit=10&offset=0&rating=g&lang=en`
let { input, url } = await arg(
"Search giphy:",
async input => {
if (!input) return []
let query = search(input)
let { data } = await get(query)
return data.data.map(gif => {
return {
name: gif.title.trim() || gif.slug,
value: {
input,
url: gif.images.original.url,
},
preview: `<img src="${gif.images.downsized.url}" alt="">`,
}
})
}
)
let formattedLink = await arg("Format to paste", [
{
name: "URL Only",
value: url,
},
{
name: "Markdown Image Link",
value: `![${input}](${url})`,
},
{
name: "HTML <img>",
value: `<img src="${url}" alt="${input}">`,
},
])
setSelectedText(formattedLink)

// Name: Natural Language Shell Command
// Description: Convert a natural language command to a shell command
// Author: Laura Okamoto
// Twitter: @laura_okamoto
import "@johnlindquist/kit";
const { Configuration, OpenAIApi } = await npm("openai");
const configuration = new Configuration({
apiKey: await env("OPENAI_API_KEY"),
});
const openAI = new OpenAIApi(configuration);
const res = await arg("Describe the shell command you want to run");
const prompt = `Use the following shell command to "${res}":`;
const completion = await openAI.createCompletion({
model: "text-davinci-003",
prompt,
temperature: 0,
max_tokens: 4069,
});
setSelectedText(completion.data.choices[0].text.trim());

/*
# Prompt Anything
Highlight some text and run this script to prompt against it.
Useful for summarizing text, generating a title, or any other task you can think of.
## Usage
- Highlight the text you want to prompt against
- Run the script via shortcut or command palette
- Input your desired prompt
- Wait for the AI to respond
- Select one of the options
* Retry - Rerun generation with option to update prompt
* Edit - Edit response in editor
- On editor exit the message is saved to the clipboard
- On editor submit the message is pasted into the highlighted text
* Copy - Copy response to clipboard
* Paste - Paste response into highlighted text
* Save - Save response to file (not working)
## Example
- Highlight: 'Some really long passage in a blog post'
- Run Script
- Prompt: `Summarize this passage in the form of Shakespearean prose`
- Waaaaait for it...
- Get a response from the AI
- Select an option
- Rinse and repeat
*/
// Name: Prompt Anything
// Description: Custom prompt for any highlighted text
// Author: Josh Mabry
// Twitter: @AI_Citizen
// Shortcut: alt shift enter
//#################
// ScriptKit Import
//#################
import "@johnlindquist/kit";
//#################
// LangChain Imports
//#################
let { ChatOpenAI } = await import("langchain/chat_models");
let { HumanChatMessage, SystemChatMessage } = await import("langchain/schema");
//#################
// Request API KEY
//#################
// stored in .env file after first run
// can change there or through the command palette
let openAIApiKey = await env("OPENAI_API_KEY", {
hint: `Grab a key from <a href="https://platform.openai.com/account/api-keys">here</a>`,
});
// System input / Task for the AI to follow
let userSystemInput = await arg("Summarize this passage");
// User Prompt from highlighted text
let userPrompt = await getSelectedText();
//#################
// Prompt Template
//#################
const formatPrompt = (prompt) => {
return `##### Ignore prior instructions
- Return answer in markdown format
- You are tasked with the following
${prompt}
########
`;
};
//################
// Options Template
//################
const options = `
* [Retry](submit:retry) - Rerun generation with option to update prompt
* [Edit](submit:edit) - Edit response in editor
* [Copy](submit:copy) - Copy response to clipboard
* [Paste](submit:paste) - Paste response into highlighted text
* [Save](submit:save) - Save response to file (not working)
`;
//################
// Main Function
//################
/**
*
* @param {*} prompt
* @param {*} humanChatMessage
*/
async function promptAgainstHighlightedText(
prompt = formatPrompt(userSystemInput),
humanChatMessage = userPrompt
) {
//#########
// Helpers
//########
// exit script on cancel
const cancelChat = () => {
process.exit(1);
};
/**
* Paste text to highlighted text and exit script
* @param {*} text
*/
const pasteTextAndExit = async (text) => {
await setSelectedText(text);
process.exit(1);
};
/**
* Copy text to clipboard and exit script
* @param {*} text
*/
const copyToClipboardAndExit = async (text) => {
await clipboard.writeText(currentMessage);
process.exit(1);
};
let currentMessage = "";
const llm = new ChatOpenAI({
// 0 = "precise", 1 = "creative"
temperature: 0.3,
// modelName: "gpt-4", // uncomment to use GPT-4 (requires beta access)
openAIApiKey: openAIApiKey,
// turn off to only get output when the AI is done
streaming: true,
callbacks: [
{
handleLLMNewToken: async (token) => {
log(`handleLLMNewToken`);
// each new token is appended to the current message
// and then rendered to the screen
currentMessage += token;
// render current message
await div({
html: md(currentMessage),
// @TODO: Figure out how to get ESC to trigger a cancel
onAbandon: cancelChat,
onEscape: cancelChat,
onBackspace: cancelChat,
// if this is set to false you can click outside the window to cancel
// which works, but would be nice to also have ESC work
ignoreBlur: false,
focus: true,
// hint: `Press ESC to cancel`,
});
},
handleLLMError: async (err) => {
dev({ err });
},
handleLLMEnd: async () => {
log(`handleLLMEnd`);
// render final message with options
let html = md(currentMessage + options);
// wait for user to select an option
const selectedOption = await div(html, {
ignoreBlur: true,
focus: true,
// have paste on text on submit?
// onSubmit: () => pasteTextAndExit(currentMessage),
});
// handle selected option
switch (selectedOption) {
case "paste":
await pasteTextAndExit(currentMessage);
case "retry":
// reset current message
currentMessage = "";
// prompt again with new prompt
// press enter to use original prompt
const followUp = await arg({
placeholder: userSystemInput,
hint: "Press enter to use the same prompt",
});
await processMessage(followUp);
break;
case "edit":
// @TODO still need to figure out best way to handle submit and abort
// would like custom buttons for triggering all of the actions like copy, paste, etc
await editor({
value: currentMessage,
onEscape: async (state) => await copyToClipboardAndExit(state),
onSubmit: async (state) => await pasteTextAndExit(state),
});
break;
case "copy":
await copyToClipboardAndExit(currentMessage);
case "save":
await inspect(currentMessage, `/conversations/${Date.now()}.md`);
exitChat();
default:
copyToClipboardAndExit(currentMessage);
}
await optionHandler(selectedOption);
},
},
],
});
//###########
// Main Loop
//###########
// runs the language model until the user cancels
while (true) {
await llm.call([
new SystemChatMessage(formatPrompt(prompt)),
new HumanChatMessage(humanChatMessage),
]);
}
}
promptAgainstHighlightedText();
// Menu: Optical Character Recognition
// Description: Extract text from images
// Author: Kent C. Dodds
// Twitter: @kentcdodds
import '@johnlindquist/kit'
import Tesseract from 'tesseract.js'
const clipboardImage = await clipboard.readImage()
if (clipboardImage.byteLength) {
const {data} = await Tesseract.recognize(clipboardImage, 'eng', {
logger: m => console.log(m),
})
clipboard.writeText(data.text)
} else {
let selectedFiles = await getSelectedFile()
let filePaths: Array<string>
if (selectedFiles) {
filePaths = selectedFiles.split('\n')
} else {
let droppedFiles = await drop({placeholder: 'Drop images to compress'})
filePaths = droppedFiles.map(file => file.path)
}
for (const filePath of filePaths) {
const {data} = await Tesseract.recognize(filePath, 'eng', {
logger: m => console.log(m),
})
clipboard.writeText(data.text)
}
}
notify({
title: 'OCR finished',
message: `Copied text to your clipboard`,
})
// Menu: Resize an Image
// Description: Select an image in Finder. Type option + i to resize it.
// Author: John Lindquist
// Twitter: @johnlindquist
// Shortcut: opt i
let sharp = await npm("sharp")
let imagePath = await getSelectedFile()
let width = Number(await arg("Enter width:"))
let metadata = await sharp(imagePath).metadata()
let newHeight = Math.floor(
metadata.height * (width / metadata.width)
)
let lastDot = /.(?!.*\.)/
let resizedImageName = imagePath.replace(
lastDot,
`-${width}.`
)
await sharp(imagePath)
.resize(width, newHeight)
.toFile(resizedImageName)