// Name: Chrome Profiles// Description: List Chrome profiles and copy their path to clipboard/open in Finder// Author: Strajkimport '@johnlindquist/kit'import { Choice } from '@johnlindquist/kit/types';const chromeAppSupportPaths = [home('Library/Application Support/Google/Chrome/'),home('Library/Application Support/Google/Chrome Canary/'),home('Library/Application Support/Google/Chrome Dev/'),]let choices: Choice[] = []for (const chromeAppSupportPath of chromeAppSupportPaths) {if (!await pathExists(chromeAppSupportPath)) continueconst subdirs = await readdir(chromeAppSupportPath)for (const subdir of subdirs) {const profilePath = path.join(chromeAppSupportPath, subdir)const profilePreferencesPath = path.join(profilePath, 'Preferences')if (!await pathExists(profilePreferencesPath)) continuetry {const preferencesRaw = await readFile(profilePreferencesPath, 'utf-8')const preferencesJson = JSON.parse(preferencesRaw)const useful = pickUsefulFromPreferences(preferencesJson)let title = useful.accountEmail || useful.profileNametitle += ` (created ${useful.profileCreationTime ? useful.profileCreationTime.toISOString().split('T')[0] : ''})`let description = profilePathlet tag = ''if (chromeAppSupportPath.includes('Google/Chrome Canary')) {tag = 'Canary'} else if (chromeAppSupportPath.includes('Google/Chrome Beta')) {tag = 'Beta'} else if (chromeAppSupportPath.includes('Google/Chrome Dev')) {tag = 'Dev'} else if (chromeAppSupportPath.includes('Google/Chrome')) {tag = 'Stable'}let className = ''if (isUnnamedProfile(title)) {className = 'text-gray-400'}choices.push({value: profilePath, // full path always as a valueimg: useful.pictureUrl || useful.avatarUrl,name: title,description,tag,nameClassName: className,})} catch (e) {console.warn(`Error parsing profile at ${profilePreferencesPath}`, e)}}}if (choices.length === 0) {await div("No Chrome profiles found")exit()}// Sort choices: regular profiles alphabetically first, then "Person X" and "Guest" profileschoices.sort((a, b) => {const aIsUnnamed = isUnnamedProfile(a.name)const bIsUnnamed = isUnnamedProfile(b.name)// If both are Unnamed profiles or both are not, sort alphabeticallyif ((aIsUnnamed && bIsUnnamed) || (!aIsUnnamed && !bIsUnnamed)) {return a.name.localeCompare(b.name)}// Put Unnamed profiles at the endreturn aIsUnnamed ? 1 : -1})let selectedProfile = await arg({placeholder: 'Select a profile',enter: 'Copy Path',actions: [{name: 'Open in Finder',onAction: async (input, { focused }) => {await revealFile(focused.value)},},]}, choices)if (selectedProfile) {await clipboard.writeText(selectedProfile)notify(`Copied Profile path to clipboard`)}// HELPERS// ===function isUnnamedProfile(title: string) {return title.match(/Person \d+|Guest/)}// Converts Windows FILETIME timestamp (100-nanosecond intervals since January 1, 1601)// to Unix timestamp (seconds since January 1, 1970)// See: https://learn.microsoft.com/en-us/windows/win32/sysinfo/file-timesfunction windowsTimestampToDate(windowsTime: number): Date {try {const n = Number(windowsTime) / 1e6 - 11644473600;return new Date(n * 1000);} catch (e) {console.warn(`Error converting Windows timestamp ${windowsTime} to Date`, e);return undefined}}function pickUsefulFromPreferences(preferencesJson: any) {return {accountEmail: preferencesJson.account_info?.[0]?.email,accountName: preferencesJson.account_info?.[0]?.full_name,pictureUrl: preferencesJson.account_info?.[0]?.picture_url,avatarUrl: avatarIdToUrl(preferencesJson.profile?.avatar_index),profileName: preferencesJson.profile?.name,profileCreationTime: windowsTimestampToDate(preferencesJson.profile?.creation_time),lastEngagementTime: windowsTimestampToDate(preferencesJson.profile?.last_engagement_time),extensionsCount: Object.keys(preferencesJson.extensions.install_signature?.ids || {}).length,}}function chromeImageUrl(name: "stable" | "canary" | "dev" | "beta") {let url = `https://raw.githubusercontent.com/chromium/chromium/refs/heads/main/chrome/app/theme/default_100_percent/common/`if (name === "stable") {url += "product_logo_16.png"} else if (name === "canary") {url += "product_logo_32_canary.png"} else if (name === "dev") {url += "product_logo_32_dev.png"} else {url += "product_logo_32_beta.png"}return url}function avatarIdToUrl(avatarId: number) {// from https://gitlab.developers.cam.ac.uk/jz448/browser-android-tabs/-/blob/base-75.0.3770.67-brave-ads/chrome/app/theme/theme_resources.grdconst mapIdToFile = {0: 'profile_avatar_generic.png',1: 'profile_avatar_generic_aqua.png',2: 'profile_avatar_generic_blue.png',3: 'profile_avatar_generic_green.png',4: 'profile_avatar_generic_orange.png',5: 'profile_avatar_generic_purple.png',6: 'profile_avatar_generic_red.png',7: 'profile_avatar_generic_yellow.png',8: 'profile_avatar_secret_agent.png',9: 'profile_avatar_superhero.png',10: 'profile_avatar_volley_ball.png',11: 'profile_avatar_businessman.png',12: 'profile_avatar_ninja.png',13: 'profile_avatar_alien.png',14: 'profile_avatar_awesome.png',15: 'profile_avatar_flower.png',16: 'profile_avatar_pizza.png',17: 'profile_avatar_soccer.png',18: 'profile_avatar_burger.png',19: 'profile_avatar_cat.png',20: 'profile_avatar_cupcake.png',21: 'profile_avatar_dog.png',22: 'profile_avatar_horse.png',23: 'profile_avatar_margarita.png',24: 'profile_avatar_note.png',25: 'profile_avatar_sun_cloud.png',26: 'profile_avatar_placeholder.png',27: 'modern_avatars/origami/avatar_cat.png',28: 'modern_avatars/origami/avatar_corgi.png',29: 'modern_avatars/origami/avatar_dragon.png',30: 'modern_avatars/origami/avatar_elephant.png',31: 'modern_avatars/origami/avatar_fox.png',32: 'modern_avatars/origami/avatar_monkey.png',33: 'modern_avatars/origami/avatar_panda.png',34: 'modern_avatars/origami/avatar_penguin.png',35: 'modern_avatars/origami/avatar_pinkbutterfly.png',36: 'modern_avatars/origami/avatar_rabbit.png',37: 'modern_avatars/origami/avatar_unicorn.png',38: 'modern_avatars/illustration/avatar_basketball.png',39: 'modern_avatars/illustration/avatar_bike.png',40: 'modern_avatars/illustration/avatar_bird.png',41: 'modern_avatars/illustration/avatar_cheese.png',42: 'modern_avatars/illustration/avatar_football.png',43: 'modern_avatars/illustration/avatar_ramen.png',44: 'modern_avatars/illustration/avatar_sunglasses.png',45: 'modern_avatars/illustration/avatar_sushi.png',46: 'modern_avatars/illustration/avatar_tamagotchi.png',47: 'modern_avatars/illustration/avatar_vinyl.png',48: 'modern_avatars/abstract/avatar_avocado.png',49: 'modern_avatars/abstract/avatar_cappuccino.png',50: 'modern_avatars/abstract/avatar_icecream.png',51: 'modern_avatars/abstract/avatar_icewater.png',52: 'modern_avatars/abstract/avatar_melon.png',53: 'modern_avatars/abstract/avatar_onigiri.png',54: 'modern_avatars/abstract/avatar_pizza.png',55: 'modern_avatars/abstract/avatar_sandwich.png'}const file = mapIdToFile[avatarId]return `https://raw.githubusercontent.com/chromium/chromium/refs/heads/main/chrome/app/theme/default_100_percent/common/${file}`}
// Name: Night Shift// Description: Control Night Shift on macOS// Author: Pavel 'Strajk' Dolecek// Acknowledgements:// - https://github.com/smudge/nightlight// - https://github.com/shmulvad/alfred-nightshift/// Notes:// nightlight CLI usage:// on/off: `nightlight on|off|toggle`, status will show current state// temp: `nightlight temp [0-100]`, no argument will show current temperature// schedule: `nightlight schedule [start|off|HH:MM HH:MM]`import "@johnlindquist/kit"import {Choice} from "@johnlindquist/kit";async function main() {await execa('which', ['nightlight']).catch(async () => {let choice = await arg({placeholder: `Nightlight CLI required, but not installed, install?`,choices: ['Yes', 'No']})if (choice !== 'Yes') {await notify(`OK, not installing Nightlight CLI & exiting`)exit() // exit whole script}console.log(`Installing Nightlight CLI... on Apple Silicon it might take a while because precompiled binaries are not available yet, see https://github.com/smudge/nightlight/issues/22`)await terminal(`brew install smudge/smudge/nightlight`)await arg("Press enter when installing in terminal finishes...")console.clear() // Clear previous console.logreturn main()})let status = execaSync('nightlight', ['status']).stdoutlet temp = execaSync('nightlight', ['temp']).stdoutlet choices: Choice[] = [{name: 'Toggle',value: 'toggle'}, {name: 'Off',value: 'off'}, {name: 'On',value: 'on'}, {name: 'On & 25%',value: 'temp 25'}, {name: 'On & 50%',value: 'temp 50'}, {name: 'On & 75%',value: 'temp 75'}, {name: 'On & 100%',value: 'temp 100'}]let choice = await arg({placeholder: `Either select or type exact amount from 0 to 100`,hint: `Current status: ${uppercaseFirst(status)} • Current temperature: ${temp}`,choices,strict: false // allow arbitrary input})let args = choice.split(' ')let isArg0Number = !isNaN(parseInt(args[0]))if (isArg0Number) {args = ['temp', args[0]]}if (args[0] === 'temp') {// if setting temp, turn on firstawait execa('nightlight', ['on'])}await execa('nightlight', args).then(() => notify(`Nightlight set to: ${choice}`)).catch(() => notify(`Failed to set Nightlight to: ${choice}`))}void main()function uppercaseFirst(str: string) {return str.charAt(0).toUpperCase() + str.slice(1)}
// Name: Faker// Description: Generate fake data with faker.js// Author: Pavel 'Strajk' Dolecek// Twitter: @straaajkimport "@johnlindquist/kit"import { faker } from '@faker-js/faker';import { Choice } from "@johnlindquist/kit";const CACHE_EXPIRATION = 1000 * 60 * 60 * 24 * 30 // 1 month// const CACHE_EXPIRATION = 1000 * 15 // 15 seconds // uncomment for debugging// @ts-ignore kitCommand is defined, but it's not in types :(const keyv = await store(kitCommand, {cachedChoices: null, cachedChoicesExpires: null})const maybeCachedChoices = await keyv.get('choices') as Choice[] | nullconst maybeCachedChoicesExpires = await keyv.get('cachedChoicesExpires') as number | nullconst EXAMPLES_MAX_COUNT = 5const EXAMPLES_MAX_CHARS = 120const modulesToIgnore = ["_defaultRefDate","_randomizer","datatype", // just boolean"helpers","rawDefinitions","definitions",]const methodsToIgnore = ["faker",]const pairsToIgnore = ["internet.userName", // deprecated in favour of internet.username"image.avatarLegacy", // deprecated in favour of image.avatar// requires params, which would require special handling"string.fromCharacters","date.between","date.betweens",]let choices = []if (maybeCachedChoices?.length > 0&& maybeCachedChoicesExpires > Date.now()) {// console.log("Using cached choices")choices = maybeCachedChoices as Choice[]} else {// console.log("Generating choices")for (const module in faker) { // date, number, string, finance, ...if (modulesToIgnore.includes(module)) {continue;}for (const method in faker[module]) {if (methodsToIgnore.includes(method)) {continue;}const pair = `${module}.${method}`;if (pairsToIgnore.includes(pair)) {continue;}let examplesCount = 0;let examplesText = "";while (examplesText.length < EXAMPLES_MAX_CHARS && examplesCount < EXAMPLES_MAX_COUNT) {const newExample = callFaker(module, method);if (examplesText.length > 0) {examplesText += " • "}examplesText += toString(newExample);examplesCount++;}examplesText = examplesText.trim().substring(0, EXAMPLES_MAX_CHARS);choices.push({name: `${module}: ${method}`,value: pair,description: examplesText,})}}await keyv.set('choices', choices)await keyv.set('cachedChoicesExpires', Date.now() + CACHE_EXPIRATION)}const selected = await arg({placeholder: "Select a Faker module and method",choices: choices,enter: "Copy to clipboard",})let [module, method] = selected.split(".")let value = toString(callFaker(module, method))await copy(value)await notify(`Copied: ${value}`)// Helpers// ===function callFaker(module: string, method: string) {try {return faker[module][method]()} catch (error) {console.error(`Error calling faker method: ${module}.${method}`, error);return}}function toString(value: any): string {if (typeof value === "string") {return value} else if (typeof value === "object") {return JSON.stringify(value)} else {return value?.toString() || "❌"}}
// Name: Karabiner Elements Profile Switcher// Acknowledgements:// - https://www.alfredforum.com/topic/9927-karabiner-elements-profile-switcher/// Notes:// - Probably could also work with cli https://github.com/raycast/extensions/blob/be03024b4c4f4f1ad0af7f4d20ea4630d7f0ee20/extensions/karabiner-profile-switcher/src/model/KarabinerManager.tsimport "@johnlindquist/kit"import fs from 'fs'const CONFIG_PATH = home('.config/karabiner/karabiner.json')const configJson = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))const profileChoices = configJson.profiles.map((profile: any) => ({name: profile.name,value: profile.name,tag: profile.selected ? '🟢' : '', // Note: Not sure if there's a better way to highlight currenly active}))const selectedProfile = await arg({placeholder: 'Select a profile',choices: profileChoices,})for (const profile of configJson.profiles) {profile.selected = profile.name === selectedProfile // intentional mutation}fs.writeFileSync(CONFIG_PATH, JSON.stringify(configJson, null, 2))notify(`Karabiner Elements profile switched to "${selectedProfile}"`)
// Name: Disposable email// Description: Generate a disposable email address, open the inbox in the browser, and copy the email address to the clipboard// Acknowledgments:// - https://www.alfredforum.com/topic/4643-temporary-email-%E2%80%94-generate-disposable-email-inboxes/import "@johnlindquist/kit"import { uniqueNamesGenerator, adjectives, animals, colors } from 'unique-names-generator';// The ones with auto: true will copy the email address to clipboard, the ones with auto: false will notconst providers = {"maildrop.cc": { name: "Maildrop", auto: true },"harakirimail.com": { name: "Harakirimail", auto: true },"incognitomail.co": { name: "Incognitomail", auto: false },"temporarymail.com": { name: "Temporarymail", auto: false },"mail.tm": { name: "Mail.tm", auto: false },"dropmail.me": { name: "Dropmail", auto: false },"guerrillamail.com": { name: "Guerrillamail", auto: false }};let provider = await arg({placeholder: `Select Provider`,choices: Object.entries(providers).map(([name, def]) => ({name: def.name,description: def.auto ? `🤖 Auto-copies email to clipboard` : `🫵 Manually copy email from website`,value: name}))});let def = providers[provider];if (!def) throw new Error(`Invalid provider: ${provider}`);if (def.auto) {const desiredEmailName = uniqueNamesGenerator({dictionaries: [adjectives, animals],length: 2,separator: '-',});const email = `${desiredEmailName}@${provider}`;let url: string;if (provider === 'maildrop.cc') {url = `https://${provider}/inbox/?mailbox=${desiredEmailName}`;} else {url = `https://${provider}/inbox/${desiredEmailName}`;}await clipboard.writeText(email);await open(url);await notify(`Email: ${email} copied to clipboard`);} else {await open(`https://${provider}`);await notify(`Get the email address from the website`);}
// Name: Clear MacOS notifications// Description: Only visible notifications – clearing not visible notifications is not possibleimport "@johnlindquist/kit"await jxa(`Application("System Events").applicationProcesses.byName("NotificationCenter").windows[0].groups[0].scrollAreas[0].uiElements[0].groups().map(banner => banner.actions().slice(-1)[0]).forEach(banner => banner.perform())`)// Inspired by Kit's "applescript" functionasync function jxa(script: string) {await writeFile(kenvTmpPath("clear-macos-notifications.jxa"), script);await execa("osascript", ["-l", "JavaScript", kenvTmpPath("clear-macos-notifications.jxa")]);}
// Name: Lyrics on Genius// Description: Look up Lyrics of Current Song on Rap Genius// Acknowledgments:// - Ryan Rudzitis: Look up Lyrics of Current Song on Rap Geniusimport "@johnlindquist/kit"let appsToTry = ["Music","Spotify"]let qs: stringfor (let app of appsToTry) {qs = await applescript(/* applescript */ `tell application "${app}"if player state is playing thenset aTrack to the current trackset aName to name of aTrackset aArtist to artist of aTrackreturn quoted form of (aArtist & " - " & aName)elsereturn ""end ifend tell`)if (qs) break}if (!qs) {notify(`No music is playing in: ` + appsToTry.join(", "))} else {open(`http://genius.com/search?q=${qs}`)}
// Name: Messages 2FA codes// Description: Search for 2FA codes in your Messages, within the last 30 minutes// Ackowledgements:// - https://github.com/squatto/alfred-imessage-2fa/// - https://github.com/raycast/extensions/tree/main/extensions/imessage-2faimport "@johnlindquist/kit"import Database from 'better-sqlite3';let preferences = {lookBackMinutes: 30,ignoreRead: false,}export type TMessage = {guid: string;message_date: string; // 2024-11-26 06:11:18sender: string; // e.g. amazon.de or +49123456789service: string; // e.g. SMStext: string;}const db = new Database(home("Library/Messages/chat.db"));let output = await arg({placeholder: "Select a message or start typing to search",choices: async (input) => {let stmt = db.prepare(dbQuery(input));let messages = stmt.all() as TMessage[];return messages.map((m) => ({name: m.text,tag: extractCode(m.text) ?? "no code",description: `${m.message_date} • ${m.sender} • ${m.service}`,value: m.text,preview: `<div class="p-2 text-sm">${m.text}</div>`,}));},actions: [{name: "Copy Whole Message",flag: "copyWholeMessage",visible: true,shortcut: `${cmd}+c`,},]})if (flag.copyWholeMessage) {clipboard.writeText(output)notify("Whole message copied to clipboard")} else {let code = extractCode(output)if (code) {clipboard.writeText(code)notify(`Code: ${code} copied to clipboard`)} else {clipboard.writeText(output)notify("No code found. Copied whole message to clipboard instead")}}// Helpers// ===function dbQuery(qs: string = "") {let baseQuery = /* sql */`selectmessage.guid,message.rowid,ifnull(handle.uncanonicalized_id, chat.chat_identifier) AS sender,message.service,datetime(message.date / 1000000000 + 978307200, 'unixepoch', 'localtime') AS message_date,message.textfrom messageleft join chat_message_join on chat_message_join.message_id = message.ROWIDleft join chat on chat.ROWID = chat_message_join.chat_idleft join handle on message.handle_id = handle.ROWIDwhere message.is_from_me = 0and message.text is not nulland length(message.text) > 0anddatetime(message.date / 1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime')>=datetime('now', '-${preferences.lookBackMinutes} minutes', 'localtime')`;if (preferences.ignoreRead) baseQuery += " and message.is_read = 0";if (!qs) { // search for codebaseQuery = /* sql */`${baseQuery} and (-- Matches 3 alphanumeric (e.g., 'ABC')message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z]*'-- Matches 4 alphanumeric (e.g., 'ABCD')or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*'-- Matches 5 alphanumeric (e.g., 'ABCDE')or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*'-- Matches 6 alphanumeric (e.g., 'ABCDEF')or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*'-- Matches format '123-456'or message.text glob '*[0-9][0-9][0-9]-[0-9][0-9][0-9]*'-- Matches 7 alphanumeric (e.g., 'ABCDEFG')or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*'-- Matches 8 alphanumeric (e.g., 'ABCDEFGH')or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*')`;} else { // Search for textbaseQuery = /* sql */`${baseQuery} and message.text like '%${qs}%'`;}return `${baseQuery} \norder by message.date desc limit 100`.trim();}export function extractCode(original: string) {// remove URLsconst urlRegex = new RegExp("\\b((https?|ftp|file):\\/\\/|www\\.)[-A-Z0-9+&@#\\/%?=~_|$!:,.;]*[A-Z0-9+&@#\\/%=~_|$]","ig");let message = original.replaceAll(urlRegex, "");if (message.trim() === "") return "";let m;let code;// Look for specific patterns firstif ((m = /^(\d{4,8})(\sis your.*code)/.exec(message)) !== null) {// 4-8 digits followed by "is your [...] code"// examples:// "2773 is your Microsoft account verification code"code = m[1];} else if ((m = /(code\s*:|is\s*:|码|use code|autoriza(?:ca|çã)o\s*:|c(?:o|ó)digo\s*:)\s*(\w{4,8})($|\s|\\R|\t|\b|\.|,)/i.exec(message)) !== null) {// "code:" OR "is:" OR "use code", optional whitespace, then 4-8 consecutive alphanumeric characters// examples:// "Your Airbnb verification code is: 1234."// "Your verification code is: 1234, use it to log in"// "Here is your authorization code:9384"// "【抖音】验证码9316,用于手机验证"// "Your healow verification code is : 7579."// "TRUSTED LOCATION PASSCODE: mifsuc"// "Código de Autorização: 12345678"code = m[2];} else {// more generic, brute force patterns// remove phone numbers// we couldn't do this before, because some auth codes resemble text shortcodes, which would be filtered by this regexconst phoneRegex = new RegExp(// https://stackoverflow.com/a/123666/(?:(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})(?:\s*(?:#|x\.?|ext\.?|extension)\s*(\d+))?/,"ig");const originalMessage = message;message = message.replaceAll(phoneRegex, "");if ((m = /(^|\s|\\R|\t|\b|G-|:)(\d{5,8})($|\s|\\R|\t|\b|\.|,)/.exec(message)) !== null) {// 5-8 consecutive digits// examples:// "您的验证码是 199035,10分钟内有效,请勿泄露"// "登录验证码:627823,您正在尝试【登录】,10分钟内有效"// "【赛验】验证码 54538"// "Enter this code to log in:59678."// "G-315643 is your Google verification code"// "Enter the code 765432, and then click the button to log in."// "Your code is 45678!"// "Your code is:98765!"code = m[2];} else if ((m = /\b(?=[A-Z]*[0-9])(?=[0-9]*[A-Z])[0-9A-Z]{3,8}\b/.exec(message)) !== null) {// 3-8 character uppercase alphanumeric string, containing at least one letter and one number// examples:// "5WGU8G"// "Your code is: 5WGU8G"// "CWGUG8"// "CWGUG8 is your code"// "7645W453"code = m[0];} else if ((m = /(^|code:|is:|\b)\s*(\d{3})-(\d{3})($|\s|\\R|\t|\b|\.|,)/i.exec(message)) !== null) {// line beginning OR "code:" OR "is:" OR word boundary, optional whitespace, 3 consecutive digits, a hyphen, then 3 consecutive digits// but NOT a phone number (###-###-####)// examples:// "123-456"// "Your Stripe verification code is: 719-839."// and make sure it isn't a phone number// doesn't match: <first digits>-<second digits>-<4 consecutive digits>const first = m[2];const second = m[3];code = `${first}${second}`;} else if ((m = /(code|is):?\s*(\d{3,8})($|\s|\\R|\t|\b|\.|,)/i.exec(originalMessage)) !== null) {// "code" OR "is" followed by an optional ":" + optional whitespace, then 3-8 consecutive digits// examples:// "Please enter code 548 on Zocdoc."code = m[2];} else {// console.log("no code found in message");}}return code;}
// Name: Keyboard Maestro// Description: Run or edit a Keyboard Maestro macro// Author: Pavel 'Strajk' Dolecek// Twitter: @straaajk// Cache: trueimport "@johnlindquist/kit"import plist from 'plist'import dedent from "dedent"export type TMacro = Partial<{name: stringuid: stringactive: booleancreated: numberused: numberenabled: booleanlastused: numbermodified: numbersaved: numbersort: stringtriggers: Array<Partial<{ description: string, short: string, type: string }>>}>export type TMacroGroup = Partial<{uid: stringenabled: booleanname: stringsort: stringmacros: TMacro[]}>let kmMacrosRaw = await applescript(dedent`tell application "Keyboard Maestro Engine"getmacros with asstringend tell`)let kmMacrosParsed = plist.parse(kmMacrosRaw) as TMacroGroup[]const choices = kmMacrosParsed.flatMap(group => group.macros.map(macro => ({name: `${group.name} - ${macro.name}`,value: macro.uid})))let chosen = await arg({placeholder: "Choose a macro",choices: choices,actions: [{shortcut: `${cmd}+e`,name: "Edit Macro",visible: true,flag: "edit"}]})if (flag.edit) { // Note that flag is a globalawait applescript(dedent`tell application "KeyboardMaestro"editMacro "${chosen}"activateend tell`)} else {await applescript(dedent`tell application "Keyboard Maestro Engine"do script "${chosen}"end tell`)}
// Name: Rename script files to match their name// Description: Renames script file names to match their name (// Name: Foo Bar -> foo-bar.ts)import "@johnlindquist/kit"import { stripName } from "@johnlindquist/kit"import * as fs from "fs/promises"import dedent from "dedent"// just for documentation purposeslet exampleScript = {"command": "rename-script-files-to-match-their-name","filePath": "/Users/strajk/.kenv/scripts/rename-script-files-to-match-their-name.ts","id": "/Users/strajk/.kenv/scripts/rename-script-files-to-match-their-name.ts","name": "Rename script files to match their name","timestamp": 1732375034783,"type": "Prompt"// ...}let scripts = await getScripts()for (let script of scripts) {let normalizedName = stripName(script.name)let extname = path.extname(script.filePath)let basename = path.basename(script.filePath, extname)if (basename !== script.command) {console.log(dedent`This should never happen, basename and command should be the same:basename: ${basename}command: ${script.command}`)notify(`basename: ${basename} command: ${script.command}`)exit()}if (normalizedName !== basename) {console.log(dedent`"${script.name}":current: ${basename}normalized: ${normalizedName}`)let confirmed = await arg({placeholder: `Rename "${basename}" to "${normalizedName}"`,choices: [{ name: "Yes", value: true },{ name: "No", value: false }]})if (!confirmed) {continue}console.log(`Renaming "${basename}" to: ${normalizedName}`)let newPath = path.join(path.dirname(script.filePath), normalizedName + extname)await fs.rename(script.filePath, newPath)}}
// Name: Install app from dmg in Downloads/// Implementation notes:/// - dmg file name can be different from mounted volume name, e.g. UIBrowser302.dmg -> /Volumes/UI Browser 3.0.2.0/// - dmg might contain License agreement that needs to be accepted, e.g. UIBrowser302.dmg/// - dmg might contain other files than just the app, e.g. Extras folder and README.rtf, see UIBrowser302.dmgimport "@johnlindquist/kit"import fs, {statSync, unlinkSync} from "fs";import {join} from "path";import * as luxon from "luxon"import {execa} from "execa";import {execSync} from "child_process"let downloadsDir = home("Downloads") // Feel free to changelet dmgPaths = await globby("*.dmg", { cwd: downloadsDir })let dmgObjs = dmgPaths.map(path => ({fullPath: join(downloadsDir, path),baseName: path.split("/").pop()?.replace(".dmg", ""),createdAt: statSync(join(downloadsDir, path)).ctime.getTime(),sizeInMb: statSync(join(downloadsDir, path)).size / 1024 / 1024})).sort((a, b) => b.createdAt - a.createdAt)if (dmgObjs.length === 0) {setPlaceholder("No DMG files found in Downloads directory")} else {let selectedDmgPath = await arg({placeholder: "Which dmg?",choices: dmgObjs.map(dmg => ({value: dmg.fullPath,name: dmg.baseName,description: `${luxon.DateTime.fromMillis(dmg.createdAt).toFormat('yyyy-MM-dd HH:mm')} • ${dmg.sizeInMb.toFixed(2)} MB`}))})console.log(`Mounting ${selectedDmgPath}`)let volumeName = await attachDmg(selectedDmgPath)let mountPath = `/Volumes/${volumeName}`;console.log(`Mounted to ${mountPath}`)// Note: Globby did not work for me for mounted volumeslet apps = fs.readdirSync(mountPath).filter(f => f.endsWith(".app"))if (apps.length === 0) {setPlaceholder("No apps found in the mounted volume")// TODO: Find a better way to do early returns/exits} else {let confirmed = await arg({placeholder: `Found ${apps.length} apps: ${apps.join(", ")}, install?`,choices: ["yes", "no"]})if (confirmed !== "yes") {notify("Aborted")process.exit(0)}for (let app of apps) {console.log(`Copying ${app} to /Applications folder`);await execa(`cp`, ['-a', `${mountPath}/${app}`,'/Applications/']);}console.log(`Detaching ${mountPath}`)await detachDmg(mountPath)let confirmDeletion = await arg({placeholder: `Delete ${selectedDmgPath}?`,choices: ["yes", "no"]})if (confirmDeletion === "yes") {console.log(`Deleting ${selectedDmgPath}`)await trash(selectedDmgPath)}}}// Helpers// ===async function attachDmg(dmgPath: string): Promise<string> {// https://superuser.com/questions/221136/bypass-a-licence-agreement-when-mounting-a-dmg-on-the-command-linelet out = execSync(`yes | PAGER=cat hdiutil attach "${dmgPath}"`).toString()let lines = out.split("\n").reverse()// from the end, find line with volume name// /dev/disk6s2 Apple_HFS /Volumes/UI Browser 3.0.2.0let lineWithVolume = lines.find(line => line.includes("/Volumes/"))if (!lineWithVolume) {throw new Error(`Failed to find volume name in output: ${out}`)}let volumeName = lineWithVolume.split(`/Volumes/`)[1]return volumeName}async function detachDmg(mountPoint: string) {await execa('hdiutil', ['detach', mountPoint])}
// Name: Log Past X Minutes to Calendar// Description: Asks for duration in minutes, event title, and (on first use) calendar name// Author: Pavel 'Strajk' Dolecek <www.strajk.me>// Twitter: @straaajk// me.strajk:status SHAREDconst minutes = await arg("Enter the duration (in minutes) to log retroactively")const title = await arg("Describe the activity you want to log")let calendar = await env("SCRIPTKIT_LOG_CALENDAR_NAME", {hint: `Enter the exact name of an existing calendar in your Calendar app`,})await applescript(`tell application "Calendar"switch view to week viewtell calendar "${calendar}"set theCurrentDate to current datemake new event at end with properties {summary:"${title}", start date:theCurrentDate - ${minutes} * minutes, end date:theCurrentDate}end tellend tell`)await notify(`Logged ${minutes} minutes of ${title} to ${calendar}`)
// Menu: Super Search across multiple websites// Description: Search multiple websites, in bulk, in your browser// Author: Pavel 'Strajk' Dolecek <www.strajk.me>// Twitter: @straaajk//// Shortcut: command option ;// me.strajk:status WIP// BEWARE: Discord search requires my userscript:// https://github.com/Strajk/setup/blob/master/user-scripts/discord-search-from-q-url-param.user.jslet templates = {discord: `https://discord.com/channels/{slug}/?q={query}`,githubRepo: `https://github.com/{slug}/issues?q={query}`,githubDiscussion: `https://github.com/{slug}/discussions?discussions_q={query}`,twitter: `https://x.com/search?q=from%3A{slug}+{query}`,reddit: `https://www.reddit.com/r/{slug}/search?q={query}`,// TODO:// Slack: But even harder than Discord https://stackoverflow.com/questions/51541986/a-way-to-open-up-slack-search-ui-in-a-browser-from-a-url}// Each topic can only use search templates defined above (discord, githubRepo, githubDiscussion)// but doesn't need to include all of them - just some are enough// ⬇⬇⬇ EDIT TO YOUR LIKING ⬇⬇⬇let topics: Record<string, Partial<{[K in keyof typeof templates]: string[]}>> = {kit: {discord: [`804053880266686464`],githubRepo: [`johnlindquist/kit`],githubDiscussion: [`johnlindquist/kit`],twitter: [`scriptkitapp`],},litellm: {discord: [`1123360753068540065`],githubRepo: [`BerriAI/litellm`],githubDiscussion: [`BerriAI/litellm`],twitter: [`LiteLLM`],},wxt: {discord: [`1212416027611365476`],githubRepo: [`wxt-dev/wxt`],githubDiscussion: [`wxt-dev/wxt`]},coolify: {discord: [`459365938081431553`],githubRepo: [`coollabsio/coolify`],githubDiscussion: [`coollabsio/coolify`],twitter: [`coolify`],},crawlee: {discord: [`801163717915574323`],githubRepo: [`apify/crawlee`],githubDiscussion: [`apify/crawlee`]},mantine: {discord: [`854810300876062770`],githubRepo: [`mantinedev/mantine`],githubDiscussion: [`orgs/mantinedev`], // note it's different from repo abovetwitter: [`mantinedev`],},supabase: {discord: [`839993398554656828`],githubDiscussion: [`orgs/supabase`],reddit: [`Supabase`],githubRepo: [`supabase/supabase`,`supabase/supabase-js`,`supabase/cli`,`supabase/postgrest-js`,`supabase/supabase-py`,],},pnpm: {discord: [`731599538665553971`],githubRepo: [`pnpm/pnpm`],githubDiscussion: [`pnpm/pnpm`],},ai: {discord: [`1110910277110743103`, // superagent`1153072414184452236`, // autogen`822583790773862470`, // latentspace`1122748573000409160`, // ai stack devs`877056448956346408`, // lablablab],},nextjs: {discord: [`752553802359505017`, // nextjs`966627436387266600`, // theo typesafe cult],},plasmo: {githubDiscussion: [`PlasmoHQ/plasmo`],githubRepo: [`PlasmoHQ/plasmo`],discord: [`946290204443025438`],},scraping: {discord: [`646150246094602263`, // scraping in prod`851364676688543744`, // scrapy`737009125862408274`, // scraping enthusiasts],}}let hint = `${isMac ? `⌘` : `Control`}+o to edit`let type = await arg({placeholder: `What topic? ${hint}`,choices: Object.keys(topics),})let query = await arg('Query?')let topicObj = topics[type] // e.g. { discord: ['123', '345'], github: ['foo'] }for (const [key, slugs] of Object.entries(topicObj)) {// key is e.g. discord, githubIssues, githubDiscussions, ...// slugs are e.g. ['12356', 'facebook/react']for (const slug of slugs) {// slug e.g. '12356', 'facebook/react'let urlTemplate = templates[key] // e.g. 'github.com/{id}/issues?q={query}'let url = urlTemplate.replace('{slug}', slug) // e.g. 'facebook/react'.replace('{query}', encodeURI(query)) // e.g. 'foo'exec(`open ${url}`)}}