@@ -57,7 +57,8 @@ const AppPreset APP_PRESETS[] = {
5757 { L" copilot" , L" GitHub Copilot" , IDI_COPILOT },
5858 { L" gemini" , L" Gemini" , IDI_GEMINI },
5959 { L" codex" , L" Codex" , IDI_CODEX },
60- { L" cursor" , L" Cursor" , IDI_CURSOR }
60+ { L" cursor" , L" Cursor" , IDI_CURSOR },
61+ { L" opencode" , L" OpenCode" , IDI_TOASTY }
6162};
6263
6364// Extract embedded PNG resource to temp file and return path
@@ -199,6 +200,11 @@ const AppPreset* check_command_line_for_preset(const std::wstring& cmdLine) {
199200 return find_preset (L" cursor" );
200201 }
201202
203+ // Check for OpenCode
204+ if (lowerCmd.find (L" opencode" ) != std::wstring::npos) {
205+ return find_preset (L" opencode" );
206+ }
207+
202208 return nullptr ;
203209}
204210
@@ -298,7 +304,7 @@ void print_usage() {
298304 << L" -i, --icon <path> Custom icon path (PNG recommended, 48x48px)\n "
299305 << L" -v, --version Show version and exit\n "
300306 << L" -h, --help Show this help\n "
301- << L" --install [agent] Install hooks for AI CLI agents (claude, gemini, copilot, or all)\n "
307+ << L" --install [agent] Install hooks for AI CLI agents (claude, gemini, copilot, codex, opencode, or all)\n "
302308 << L" --uninstall Remove hooks from all AI CLI agents\n "
303309 << L" --status Show installation status\n "
304310 << L" --register Re-register app for notifications (troubleshooting)\n "
@@ -876,6 +882,16 @@ bool detect_copilot() {
876882 return path_exists (L" .github\\ hooks" ) || path_exists (L" .github" );
877883}
878884
885+ bool detect_codex () {
886+ std::wstring codexPath = expand_env (L" %USERPROFILE%\\ .codex" );
887+ return path_exists (codexPath);
888+ }
889+
890+ bool detect_opencode () {
891+ std::wstring opencodePath = expand_env (L" %USERPROFILE%\\ .config\\ opencode" );
892+ return path_exists (opencodePath);
893+ }
894+
879895// Read file content as string
880896std::string read_file (const std::wstring& path) {
881897 std::ifstream file (path, std::ios::binary);
@@ -1154,6 +1170,116 @@ bool install_copilot(const std::wstring& exePath) {
11541170 return write_file (configPath, jsonStr);
11551171}
11561172
1173+ // Install hook for OpenAI Codex CLI
1174+ // Codex uses TOML config at ~/.codex/config.toml with notify = "command"
1175+ bool install_codex (const std::wstring& exePath) {
1176+ std::wstring configDir = expand_env (L" %USERPROFILE%\\ .codex" );
1177+ std::wstring configPath = configDir + L" \\ config.toml" ;
1178+
1179+ // Create .codex directory if needed
1180+ fs::create_directories (configDir);
1181+
1182+ std::string content = read_file (configPath);
1183+
1184+ // Escape backslashes for TOML string
1185+ std::string exePathUtf8;
1186+ {
1187+ int size = WideCharToMultiByte (CP_UTF8, 0 , exePath.c_str (), -1 , nullptr , 0 , nullptr , nullptr );
1188+ if (size > 0 ) {
1189+ exePathUtf8.resize (size - 1 );
1190+ WideCharToMultiByte (CP_UTF8, 0 , exePath.c_str (), -1 , &exePathUtf8[0 ], size, nullptr , nullptr );
1191+ }
1192+ }
1193+ // Double backslashes for TOML
1194+ std::string escapedPath;
1195+ for (char c : exePathUtf8) {
1196+ if (c == ' \\ ' ) escapedPath += " \\\\ " ;
1197+ else escapedPath += c;
1198+ }
1199+
1200+ std::string notifyLine = " notify = [\" " + escapedPath + " \" , \" Codex finished\" , \" -t\" , \" Codex\" ]\n " ;
1201+
1202+ if (content.empty ()) {
1203+ // New file
1204+ return write_file (configPath, notifyLine);
1205+ }
1206+
1207+ // Check if notify is already set
1208+ if (content.find (" toasty" ) != std::string::npos) {
1209+ return true ; // Already installed
1210+ }
1211+
1212+ backup_file (configPath);
1213+
1214+ // Replace existing notify line or append
1215+ size_t notifyPos = content.find (" notify" );
1216+ if (notifyPos != std::string::npos) {
1217+ // Find end of line
1218+ size_t lineEnd = content.find (' \n ' , notifyPos);
1219+ if (lineEnd == std::string::npos) lineEnd = content.size ();
1220+ else lineEnd++; // include the newline
1221+ content.replace (notifyPos, lineEnd - notifyPos, notifyLine);
1222+ } else {
1223+ // Append
1224+ if (!content.empty () && content.back () != ' \n ' ) content += ' \n ' ;
1225+ content += notifyLine;
1226+ }
1227+
1228+ return write_file (configPath, content);
1229+ }
1230+
1231+ // Install plugin for OpenCode
1232+ // OpenCode uses JS plugins at ~/.config/opencode/plugins/
1233+ bool install_opencode (const std::wstring& exePath) {
1234+ std::wstring pluginsDir = expand_env (L" %USERPROFILE%\\ .config\\ opencode\\ plugins" );
1235+ std::wstring pluginPath = pluginsDir + L" \\ toasty.js" ;
1236+
1237+ fs::create_directories (pluginsDir);
1238+
1239+ if (path_exists (pluginPath)) {
1240+ std::string content = read_file (pluginPath);
1241+ if (content.find (" toasty" ) != std::string::npos) {
1242+ return true ; // Already installed
1243+ }
1244+ }
1245+
1246+ // Convert exe path to JS-safe string (forward slashes)
1247+ std::string exePathUtf8;
1248+ {
1249+ int size = WideCharToMultiByte (CP_UTF8, 0 , exePath.c_str (), -1 , nullptr , 0 , nullptr , nullptr );
1250+ if (size > 0 ) {
1251+ exePathUtf8.resize (size - 1 );
1252+ WideCharToMultiByte (CP_UTF8, 0 , exePath.c_str (), -1 , &exePathUtf8[0 ], size, nullptr , nullptr );
1253+ }
1254+ }
1255+ // Escape backslashes for JS string
1256+ std::string jsPath;
1257+ for (char c : exePathUtf8) {
1258+ if (c == ' \\ ' ) jsPath += " \\\\ " ;
1259+ else jsPath += c;
1260+ }
1261+
1262+ std::string plugin =
1263+ " // Toasty notification plugin for OpenCode\n "
1264+ " const { execSync } = require('child_process');\n "
1265+ " \n "
1266+ " module.exports = {\n "
1267+ " name: 'toasty',\n "
1268+ " description: 'Toast notification when OpenCode finishes',\n "
1269+ " onComplete: () => {\n "
1270+ " try {\n "
1271+ " execSync('\" " + jsPath + " \" \" OpenCode finished\" -t \" OpenCode\" ', { timeout: 5000 });\n "
1272+ " } catch (e) { /* ignore */ }\n "
1273+ " }\n "
1274+ " };\n " ;
1275+
1276+ if (path_exists (pluginPath)) {
1277+ backup_file (pluginPath);
1278+ }
1279+
1280+ return write_file (pluginPath, plugin);
1281+ }
1282+
11571283// Check if Claude hook is installed
11581284bool is_claude_installed () {
11591285 std::wstring configPath = expand_env (L" %USERPROFILE%\\ .claude\\ settings.json" );
@@ -1371,24 +1497,87 @@ bool uninstall_copilot() {
13711497 return true ;
13721498}
13731499
1500+ // Uninstall hook for OpenAI Codex
1501+ bool uninstall_codex () {
1502+ std::wstring configPath = expand_env (L" %USERPROFILE%\\ .codex\\ config.toml" );
1503+ std::string content = read_file (configPath);
1504+
1505+ if (content.empty ()) return true ;
1506+
1507+ if (content.find (" toasty" ) == std::string::npos) return true ;
1508+
1509+ backup_file (configPath);
1510+
1511+ // Remove the notify line containing toasty
1512+ std::string result;
1513+ std::istringstream stream (content);
1514+ std::string line;
1515+ while (std::getline (stream, line)) {
1516+ if (line.find (" notify" ) != std::string::npos && line.find (" toasty" ) != std::string::npos) {
1517+ continue ; // Skip toasty notify line
1518+ }
1519+ result += line + ' \n ' ;
1520+ }
1521+
1522+ return write_file (configPath, result);
1523+ }
1524+
1525+ // Uninstall plugin for OpenCode
1526+ bool uninstall_opencode () {
1527+ std::wstring pluginPath = expand_env (L" %USERPROFILE%\\ .config\\ opencode\\ plugins\\ toasty.js" );
1528+
1529+ try {
1530+ if (path_exists (pluginPath)) {
1531+ backup_file (pluginPath);
1532+ fs::remove (pluginPath);
1533+ }
1534+ } catch (const std::exception&) {
1535+ std::wcerr << L" Error uninstalling OpenCode plugin\n " ;
1536+ return false ;
1537+ }
1538+
1539+ return true ;
1540+ }
1541+
1542+ // Check if Codex hook is installed
1543+ bool is_codex_installed () {
1544+ std::wstring configPath = expand_env (L" %USERPROFILE%\\ .codex\\ config.toml" );
1545+ std::string content = read_file (configPath);
1546+ return content.find (" toasty" ) != std::string::npos;
1547+ }
1548+
1549+ // Check if OpenCode plugin is installed
1550+ bool is_opencode_installed () {
1551+ std::wstring pluginPath = expand_env (L" %USERPROFILE%\\ .config\\ opencode\\ plugins\\ toasty.js" );
1552+ if (!path_exists (pluginPath)) return false ;
1553+ std::string content = read_file (pluginPath);
1554+ return content.find (" toasty" ) != std::string::npos;
1555+ }
1556+
13741557// Show installation status
13751558void show_status () {
13761559 std::wcout << L" Installation status:\n\n " ;
13771560
13781561 bool claudeDetected = detect_claude ();
13791562 bool geminiDetected = detect_gemini ();
13801563 bool copilotDetected = detect_copilot ();
1564+ bool codexDetected = detect_codex ();
1565+ bool opencodeDetected = detect_opencode ();
13811566
13821567 std::wcout << L" Detected agents:\n " ;
13831568 std::wcout << L" " << (claudeDetected ? L" [x]" : L" [ ]" ) << L" Claude Code\n " ;
13841569 std::wcout << L" " << (geminiDetected ? L" [x]" : L" [ ]" ) << L" Gemini CLI\n " ;
13851570 std::wcout << L" " << (copilotDetected ? L" [x]" : L" [ ]" ) << L" GitHub Copilot (in current repo)\n " ;
1571+ std::wcout << L" " << (codexDetected ? L" [x]" : L" [ ]" ) << L" OpenAI Codex\n " ;
1572+ std::wcout << L" " << (opencodeDetected ? L" [x]" : L" [ ]" ) << L" OpenCode\n " ;
13861573 std::wcout << L" \n " ;
13871574
13881575 std::wcout << L" Installed hooks:\n " ;
13891576 std::wcout << L" " << (is_claude_installed () ? L" [x]" : L" [ ]" ) << L" Claude Code\n " ;
13901577 std::wcout << L" " << (is_gemini_installed () ? L" [x]" : L" [ ]" ) << L" Gemini CLI\n " ;
13911578 std::wcout << L" " << (is_copilot_installed () ? L" [x]" : L" [ ]" ) << L" GitHub Copilot\n " ;
1579+ std::wcout << L" " << (is_codex_installed () ? L" [x]" : L" [ ]" ) << L" OpenAI Codex\n " ;
1580+ std::wcout << L" " << (is_opencode_installed () ? L" [x]" : L" [ ]" ) << L" OpenCode\n " ;
13921581}
13931582
13941583// Handle --install command
@@ -1405,12 +1594,16 @@ void handle_install(const std::wstring& agent) {
14051594 bool installClaude = installAll || agent == L" claude" ;
14061595 bool installGemini = installAll || agent == L" gemini" ;
14071596 bool installCopilot = installAll || agent == L" copilot" ;
1597+ bool installCodex = installAll || agent == L" codex" ;
1598+ bool installOpencode = installAll || agent == L" opencode" ;
14081599
14091600 if (g_dryRun) {
14101601 std::wcout << L" [dry-run] Install targets:" ;
14111602 if (installClaude) std::wcout << L" claude" ;
14121603 if (installGemini) std::wcout << L" gemini" ;
14131604 if (installCopilot) std::wcout << L" copilot" ;
1605+ if (installCodex) std::wcout << L" codex" ;
1606+ if (installOpencode) std::wcout << L" opencode" ;
14141607 std::wcout << L" \n " ;
14151608
14161609 if (installClaude) {
@@ -1432,6 +1625,16 @@ void handle_install(const std::wstring& agent) {
14321625 std::wcout << L" [dry-run] Hook command: toasty 'Copilot finished' -t 'GitHub Copilot'\n " ;
14331626 std::wcout << L" [dry-run] Hook type: sessionEnd\n " ;
14341627 }
1628+ if (installCodex) {
1629+ std::wstring configPath = expand_env (L" %USERPROFILE%\\ .codex\\ config.toml" );
1630+ std::wcout << L" [dry-run] Would write: " << configPath << L" \n " ;
1631+ std::wcout << L" [dry-run] Hook type: notify\n " ;
1632+ }
1633+ if (installOpencode) {
1634+ std::wstring pluginPath = expand_env (L" %USERPROFILE%\\ .config\\ opencode\\ plugins\\ toasty.js" );
1635+ std::wcout << L" [dry-run] Would write: " << pluginPath << L" \n " ;
1636+ std::wcout << L" [dry-run] Hook type: JS plugin\n " ;
1637+ }
14351638 return ;
14361639 }
14371640
@@ -1440,10 +1643,14 @@ void handle_install(const std::wstring& agent) {
14401643 bool claudeDetected = detect_claude ();
14411644 bool geminiDetected = detect_gemini ();
14421645 bool copilotDetected = detect_copilot ();
1646+ bool codexDetected = detect_codex ();
1647+ bool opencodeDetected = detect_opencode ();
14431648
14441649 std::wcout << L" " << (claudeDetected ? L" [x]" : L" [ ]" ) << L" Claude Code found\n " ;
14451650 std::wcout << L" " << (geminiDetected ? L" [x]" : L" [ ]" ) << L" Gemini CLI found\n " ;
14461651 std::wcout << L" " << (copilotDetected ? L" [x]" : L" [ ]" ) << L" GitHub Copilot (in current repo)\n " ;
1652+ std::wcout << L" " << (codexDetected ? L" [x]" : L" [ ]" ) << L" OpenAI Codex found\n " ;
1653+ std::wcout << L" " << (opencodeDetected ? L" [x]" : L" [ ]" ) << L" OpenCode found\n " ;
14471654 std::wcout << L" \n " ;
14481655
14491656 std::wcout << L" Installing toasty hooks...\n " ;
@@ -1479,6 +1686,24 @@ void handle_install(const std::wstring& agent) {
14791686 }
14801687 }
14811688
1689+ if (installCodex && (codexDetected || explicitAgent)) {
1690+ if (install_codex (exePath)) {
1691+ std::wcout << L" [x] OpenAI Codex: Added notify hook\n " ;
1692+ anyInstalled = true ;
1693+ } else {
1694+ std::wcout << L" [ ] OpenAI Codex: Failed to install\n " ;
1695+ }
1696+ }
1697+
1698+ if (installOpencode && (opencodeDetected || explicitAgent)) {
1699+ if (install_opencode (exePath)) {
1700+ std::wcout << L" [x] OpenCode: Added toasty.js plugin\n " ;
1701+ anyInstalled = true ;
1702+ } else {
1703+ std::wcout << L" [ ] OpenCode: Failed to install\n " ;
1704+ }
1705+ }
1706+
14821707 if (anyInstalled) {
14831708 std::wcout << L" \n Done! You'll get notifications when AI agents finish.\n " ;
14841709 } else {
@@ -1492,9 +1717,13 @@ void handle_uninstall() {
14921717 std::wcout << L" [dry-run] Would check and remove hooks from:\n " ;
14931718 std::wstring claudePath = expand_env (L" %USERPROFILE%\\ .claude\\ settings.json" );
14941719 std::wstring geminiPath = expand_env (L" %USERPROFILE%\\ .gemini\\ settings.json" );
1720+ std::wstring codexPath = expand_env (L" %USERPROFILE%\\ .codex\\ config.toml" );
1721+ std::wstring opencodePath = expand_env (L" %USERPROFILE%\\ .config\\ opencode\\ plugins\\ toasty.js" );
14951722 std::wcout << L" [dry-run] Claude: " << claudePath << L" \n " ;
14961723 std::wcout << L" [dry-run] Gemini: " << geminiPath << L" \n " ;
14971724 std::wcout << L" [dry-run] Copilot: .github\\ hooks\\ toasty.json\n " ;
1725+ std::wcout << L" [dry-run] Codex: " << codexPath << L" \n " ;
1726+ std::wcout << L" [dry-run] OpenCode: " << opencodePath << L" \n " ;
14981727 return ;
14991728 }
15001729
@@ -1529,6 +1758,24 @@ void handle_uninstall() {
15291758 }
15301759 }
15311760
1761+ if (is_codex_installed ()) {
1762+ if (uninstall_codex ()) {
1763+ std::wcout << L" [x] OpenAI Codex: Removed notify hook\n " ;
1764+ anyUninstalled = true ;
1765+ } else {
1766+ std::wcout << L" [ ] OpenAI Codex: Failed to remove\n " ;
1767+ }
1768+ }
1769+
1770+ if (is_opencode_installed ()) {
1771+ if (uninstall_opencode ()) {
1772+ std::wcout << L" [x] OpenCode: Removed toasty.js plugin\n " ;
1773+ anyUninstalled = true ;
1774+ } else {
1775+ std::wcout << L" [ ] OpenCode: Failed to remove\n " ;
1776+ }
1777+ }
1778+
15321779 if (anyUninstalled) {
15331780 std::wcout << L" \n Done! Hooks have been removed.\n " ;
15341781 } else {
@@ -1757,7 +2004,7 @@ int wmain(int argc, wchar_t* argv[]) {
17572004 explicitApp = true ;
17582005 } else {
17592006 std::wcerr << L" Error: Unknown app preset '" << appName << L" '\n " ;
1760- std::wcerr << L" Available presets: claude, copilot, gemini, codex, cursor\n " ;
2007+ std::wcerr << L" Available presets: claude, copilot, gemini, codex, cursor, opencode \n " ;
17612008 return 1 ;
17622009 }
17632010 } else {
0 commit comments