Pavel 'Strajk' Dolecek

Pavel 'Strajk' Dolecek

// Name: Chrome Profiles
// Description: List Chrome profiles and copy their path to clipboard/open in Finder
// Author: Strajk
import '@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)) continue
const 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)) continue
try {
const preferencesRaw = await readFile(profilePreferencesPath, 'utf-8')
const preferencesJson = JSON.parse(preferencesRaw)
const useful = pickUsefulFromPreferences(preferencesJson)
let title = useful.accountEmail || useful.profileName
title += ` (created ${useful.profileCreationTime ? useful.profileCreationTime.toISOString().split('T')[0] : ''})`
let description = profilePath
let 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 value
img: 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" profiles
choices.sort((a, b) => {
const aIsUnnamed = isUnnamedProfile(a.name)
const bIsUnnamed = isUnnamedProfile(b.name)
// If both are Unnamed profiles or both are not, sort alphabetically
if ((aIsUnnamed && bIsUnnamed) || (!aIsUnnamed && !bIsUnnamed)) {
return a.name.localeCompare(b.name)
}
// Put Unnamed profiles at the end
return 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-times
function 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.grd
const 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.log
return main()
})
let status = execaSync('nightlight', ['status']).stdout
let temp = execaSync('nightlight', ['temp']).stdout
let 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 first
await 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: @straaajk
import "@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[] | null
const maybeCachedChoicesExpires = await keyv.get('cachedChoicesExpires') as number | null
const EXAMPLES_MAX_COUNT = 5
const EXAMPLES_MAX_CHARS = 120
const 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.ts
import "@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 not
const 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 possible
import "@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" function
async 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 Genius
import "@johnlindquist/kit"
let appsToTry = [
"Music",
"Spotify"
]
let qs: string
for (let app of appsToTry) {
qs = await applescript(/* applescript */ `
tell application "${app}"
if player state is playing then
set aTrack to the current track
set aName to name of aTrack
set aArtist to artist of aTrack
return quoted form of (aArtist & " - " & aName)
else
return ""
end if
end 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-2fa
import "@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:18
sender: string; // e.g. amazon.de or +49123456789
service: string; // e.g. SMS
text: 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 */`
select
message.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.text
from message
left join chat_message_join on chat_message_join.message_id = message.ROWID
left join chat on chat.ROWID = chat_message_join.chat_id
left join handle on message.handle_id = handle.ROWID
where message.is_from_me = 0
and message.text is not null
and length(message.text) > 0
and
datetime(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 code
baseQuery = /* 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 text
baseQuery = /* sql */`${baseQuery} and message.text like '%${qs}%'`;
}
return `${baseQuery} \norder by message.date desc limit 100`.trim();
}
export function extractCode(original: string) {
// remove URLs
const 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 first
if ((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 regex
const 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: true
import "@johnlindquist/kit"
import plist from 'plist'
import dedent from "dedent"
export type TMacro = Partial<{
name: string
uid: string
active: boolean
created: number
used: number
enabled: boolean
lastused: number
modified: number
saved: number
sort: string
triggers: Array<Partial<{ description: string, short: string, type: string }>>
}>
export type TMacroGroup = Partial<{
uid: string
enabled: boolean
name: string
sort: string
macros: TMacro[]
}>
let kmMacrosRaw = await applescript(dedent`
tell application "Keyboard Maestro Engine"
getmacros with asstring
end 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 global
await applescript(dedent`
tell application "Keyboard
Maestro"
editMacro "${chosen}"
activate
end 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 purposes
let 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.dmg
import "@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 change
let 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 volumes
let 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-line
let 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.0
let 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 SHARED
const 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 view
tell calendar "${calendar}"
set theCurrentDate to current date
make new event at end with properties {summary:"${title}", start date:theCurrentDate - ${minutes} * minutes, end date:theCurrentDate}
end tell
end 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.js
let 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 above
twitter: [`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}`)
}
}