mirror of
https://github.com/openai/codex.git
synced 2026-02-05 08:23:41 +00:00
Compare commits
2 Commits
remove/doc
...
dev/javi/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11028280dc | ||
|
|
84a3cddc76 |
12
.github/dotslash-config.json
vendored
12
.github/dotslash-config.json
vendored
@@ -55,6 +55,18 @@
|
||||
"path": "codex-responses-api-proxy.exe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codex-notifier": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
"regex": "^codex-notifier-aarch64-apple-darwin\\.zst$",
|
||||
"path": "codex-notifier"
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"regex": "^codex-notifier-x86_64-apple-darwin\\.zst$",
|
||||
"path": "codex-notifier"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
.github/workflows/rust-release.yml
vendored
17
.github/workflows/rust-release.yml
vendored
@@ -100,6 +100,10 @@ jobs:
|
||||
- name: Cargo build
|
||||
run: cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy
|
||||
|
||||
- if: ${{ matrix.runner == 'macos-15-xlarge' }}
|
||||
name: Build notifier helper
|
||||
run: cargo build --target ${{ matrix.target }} --release -p codex-notifier
|
||||
|
||||
- if: ${{ matrix.runner == 'macos-15-xlarge' }}
|
||||
name: Configure Apple code signing
|
||||
shell: bash
|
||||
@@ -201,7 +205,12 @@ jobs:
|
||||
keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
|
||||
fi
|
||||
|
||||
for binary in codex codex-responses-api-proxy; do
|
||||
binaries=(codex codex-responses-api-proxy)
|
||||
if [[ -f "target/${{ matrix.target }}/release/codex-notifier" ]]; then
|
||||
binaries+=(codex-notifier)
|
||||
fi
|
||||
|
||||
for binary in "${binaries[@]}"; do
|
||||
path="target/${{ matrix.target }}/release/${binary}"
|
||||
codesign --force --options runtime --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
|
||||
done
|
||||
@@ -268,6 +277,9 @@ jobs:
|
||||
|
||||
notarize_binary "codex"
|
||||
notarize_binary "codex-responses-api-proxy"
|
||||
if [[ -f "target/${{ matrix.target }}/release/codex-notifier" ]]; then
|
||||
notarize_binary "codex-notifier"
|
||||
fi
|
||||
|
||||
- name: Stage artifacts
|
||||
shell: bash
|
||||
@@ -281,6 +293,9 @@ jobs:
|
||||
else
|
||||
cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
|
||||
cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}"
|
||||
if [[ -f target/${{ matrix.target }}/release/codex-notifier ]]; then
|
||||
cp target/${{ matrix.target }}/release/codex-notifier "$dest/codex-notifier-${{ matrix.target }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
- if: ${{ matrix.runner == 'windows-11-arm' }}
|
||||
|
||||
@@ -16,13 +16,14 @@ RESPONSES_API_PROXY_NPM_ROOT = REPO_ROOT / "codex-rs" / "responses-api-proxy" /
|
||||
CODEX_SDK_ROOT = REPO_ROOT / "sdk" / "typescript"
|
||||
|
||||
PACKAGE_NATIVE_COMPONENTS: dict[str, list[str]] = {
|
||||
"codex": ["codex", "rg"],
|
||||
"codex": ["codex", "codex-notifier", "rg"],
|
||||
"codex-responses-api-proxy": ["codex-responses-api-proxy"],
|
||||
"codex-sdk": ["codex"],
|
||||
}
|
||||
COMPONENT_DEST_DIR: dict[str, str] = {
|
||||
"codex": "codex",
|
||||
"codex-responses-api-proxy": "codex-responses-api-proxy",
|
||||
"codex-notifier": "codex-notifier",
|
||||
"rg": "path",
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,11 @@ BINARY_TARGETS = (
|
||||
@dataclass(frozen=True)
|
||||
class BinaryComponent:
|
||||
artifact_prefix: str # matches the artifact filename prefix (e.g. codex-<target>.zst)
|
||||
dest_dir: str # directory under vendor/<target>/ where the binary is installed
|
||||
binary_basename: str # executable name inside dest_dir (before optional .exe)
|
||||
dest_dir: str # directory under vendor/<target>/ where the binary/artifact is installed
|
||||
binary_basename: str # executable or directory name inside dest_dir (before optional .exe)
|
||||
archive_format: str = "zst" # one of "zst", "zip", "tar.gz"
|
||||
archive_member: str | None = None # optional archive member to extract
|
||||
supported_targets: tuple[str, ...] | None = None # restrict installation to these targets
|
||||
|
||||
|
||||
BINARY_COMPONENTS = {
|
||||
@@ -49,6 +52,15 @@ BINARY_COMPONENTS = {
|
||||
dest_dir="codex-responses-api-proxy",
|
||||
binary_basename="codex-responses-api-proxy",
|
||||
),
|
||||
"codex-notifier": BinaryComponent(
|
||||
artifact_prefix="codex-notifier",
|
||||
dest_dir="codex-notifier",
|
||||
binary_basename="codex-notifier",
|
||||
supported_targets=(
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-darwin",
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
RG_TARGET_PLATFORM_PAIRS: list[tuple[str, str]] = [
|
||||
@@ -101,7 +113,7 @@ def main() -> int:
|
||||
vendor_dir = codex_cli_root / VENDOR_DIR_NAME
|
||||
vendor_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
components = args.components or ["codex", "rg"]
|
||||
components = args.components or ["codex", "codex-notifier", "rg"]
|
||||
|
||||
workflow_url = (args.workflow_url or DEFAULT_WORKFLOW_URL).strip()
|
||||
if not workflow_url:
|
||||
@@ -218,11 +230,19 @@ def install_binary_components(
|
||||
return
|
||||
|
||||
for component in selected_components:
|
||||
component_targets = [
|
||||
target
|
||||
for target in targets
|
||||
if not component.supported_targets or target in component.supported_targets
|
||||
]
|
||||
if not component_targets:
|
||||
continue
|
||||
|
||||
print(
|
||||
f"Installing {component.binary_basename} binaries for targets: "
|
||||
+ ", ".join(targets)
|
||||
+ ", ".join(component_targets)
|
||||
)
|
||||
max_workers = min(len(targets), max(1, (os.cpu_count() or 1)))
|
||||
max_workers = min(len(component_targets), max(1, (os.cpu_count() or 1)))
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = {
|
||||
executor.submit(
|
||||
@@ -232,7 +252,7 @@ def install_binary_components(
|
||||
target,
|
||||
component,
|
||||
): target
|
||||
for target in targets
|
||||
for target in component_targets
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
installed_path = future.result()
|
||||
@@ -246,7 +266,7 @@ def _install_single_binary(
|
||||
component: BinaryComponent,
|
||||
) -> Path:
|
||||
artifact_subdir = artifacts_dir / target
|
||||
archive_name = _archive_name_for_target(component.artifact_prefix, target)
|
||||
archive_name = _archive_name_for_target(component, target)
|
||||
archive_path = artifact_subdir / archive_name
|
||||
if not archive_path.exists():
|
||||
raise FileNotFoundError(f"Expected artifact not found: {archive_path}")
|
||||
@@ -255,20 +275,29 @@ def _install_single_binary(
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
binary_name = (
|
||||
f"{component.binary_basename}.exe" if "windows" in target else component.binary_basename
|
||||
f"{component.binary_basename}.exe"
|
||||
if "windows" in target and component.archive_format == "zst"
|
||||
else component.binary_basename
|
||||
)
|
||||
dest = dest_dir / binary_name
|
||||
dest.unlink(missing_ok=True)
|
||||
extract_archive(archive_path, "zst", None, dest)
|
||||
if "windows" not in target:
|
||||
extract_archive(
|
||||
archive_path, component.archive_format, component.archive_member, dest
|
||||
)
|
||||
if "windows" not in target and dest.is_file():
|
||||
dest.chmod(0o755)
|
||||
return dest
|
||||
|
||||
|
||||
def _archive_name_for_target(artifact_prefix: str, target: str) -> str:
|
||||
def _archive_name_for_target(component: BinaryComponent, target: str) -> str:
|
||||
fmt = component.archive_format
|
||||
if fmt == "zip":
|
||||
return f"{component.artifact_prefix}-{target}.zip"
|
||||
if fmt == "tar.gz":
|
||||
return f"{component.artifact_prefix}-{target}.tar.gz"
|
||||
if "windows" in target:
|
||||
return f"{artifact_prefix}-{target}.exe.zst"
|
||||
return f"{artifact_prefix}-{target}.zst"
|
||||
return f"{component.artifact_prefix}-{target}.exe.zst"
|
||||
return f"{component.artifact_prefix}-{target}.zst"
|
||||
|
||||
|
||||
def _fetch_single_rg(
|
||||
@@ -346,16 +375,25 @@ def extract_archive(
|
||||
return
|
||||
|
||||
if archive_format == "zip":
|
||||
if not archive_member:
|
||||
raise RuntimeError("Missing 'path' for zip archive in DotSlash manifest.")
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
try:
|
||||
with archive.open(archive_member) as src, open(dest, "wb") as out:
|
||||
shutil.copyfileobj(src, out)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"Entry '{archive_member}' not found in archive {archive_path}."
|
||||
) from exc
|
||||
if archive_member:
|
||||
try:
|
||||
with archive.open(archive_member) as src, open(dest, "wb") as out:
|
||||
shutil.copyfileobj(src, out)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError(
|
||||
f"Entry '{archive_member}' not found in archive {archive_path}."
|
||||
) from exc
|
||||
else:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_str:
|
||||
tmp_dir = Path(tmp_dir_str)
|
||||
archive.extractall(tmp_dir)
|
||||
entries = [p for p in tmp_dir.iterdir() if not p.name.startswith(".__")]
|
||||
if len(entries) != 1:
|
||||
raise RuntimeError(
|
||||
f"Expected a single top-level entry in {archive_path}, found {len(entries)}"
|
||||
)
|
||||
shutil.move(str(entries[0]), dest)
|
||||
return
|
||||
|
||||
raise RuntimeError(f"Unsupported archive format '{archive_format}'.")
|
||||
|
||||
34
codex-rs/Cargo.lock
generated
34
codex-rs/Cargo.lock
generated
@@ -614,6 +614,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
@@ -1086,6 +1095,7 @@ dependencies = [
|
||||
"keyring",
|
||||
"landlock",
|
||||
"libc",
|
||||
"mac-notification-sys",
|
||||
"maplit",
|
||||
"mcp-types",
|
||||
"openssl-sys",
|
||||
@@ -1289,6 +1299,16 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-notifier"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-ollama"
|
||||
version = "0.0.0"
|
||||
@@ -3616,6 +3636,18 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
@@ -4061,6 +4093,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ members = [
|
||||
"cloud-tasks",
|
||||
"cloud-tasks-client",
|
||||
"cli",
|
||||
"notifier",
|
||||
"common",
|
||||
"core",
|
||||
"exec",
|
||||
|
||||
@@ -46,7 +46,7 @@ Use `codex mcp` to add/list/get/remove MCP server launchers defined in `config.t
|
||||
|
||||
### Notifications
|
||||
|
||||
You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS.
|
||||
On macOS, the CLI installs a signed helper app (`~/.codex/bin/CodexNotifier.app`) that delivers Notification Center alerts with the Codex icon, so you’ll get trusted banners as soon as a turn completes—no extra tooling required. Set `CODEX_NOTIFIER_APP` if you need to point at a custom build. If you want to customise the behaviour or are on another platform, you can still point `notify` at your own script; see the [notify documentation](../docs/config.md#notify) for an example.
|
||||
|
||||
### `codex exec` to run Codex programmatically/non-interactively
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ seccompiler = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
mac-notification-sys = "=0.6.6"
|
||||
|
||||
# Build OpenSSL from source for musl builds.
|
||||
[target.x86_64-unknown-linux-musl.dependencies]
|
||||
|
||||
BIN
codex-rs/core/assets/codex-notification.png
Normal file
BIN
codex-rs/core/assets/codex-notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
@@ -2,9 +2,11 @@ use serde::Serialize;
|
||||
use tracing::error;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UserNotifier {
|
||||
notify_command: Option<Vec<String>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
native: macos::MacNotifier,
|
||||
}
|
||||
|
||||
impl UserNotifier {
|
||||
@@ -12,14 +14,19 @@ impl UserNotifier {
|
||||
if let Some(notify_command) = &self.notify_command
|
||||
&& !notify_command.is_empty()
|
||||
{
|
||||
self.invoke_notify(notify_command, notification)
|
||||
if self.invoke_notify(notify_command, notification) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
self.native.notify(notification);
|
||||
}
|
||||
|
||||
fn invoke_notify(&self, notify_command: &[String], notification: &UserNotification) {
|
||||
fn invoke_notify(&self, notify_command: &[String], notification: &UserNotification) -> bool {
|
||||
let Ok(json) = serde_json::to_string(¬ification) else {
|
||||
error!("failed to serialise notification payload");
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut command = std::process::Command::new(¬ify_command[0]);
|
||||
@@ -29,18 +36,30 @@ impl UserNotifier {
|
||||
command.arg(json);
|
||||
|
||||
// Fire-and-forget – we do not wait for completion.
|
||||
if let Err(e) = command.spawn() {
|
||||
warn!("failed to spawn notifier '{}': {e}", notify_command[0]);
|
||||
match command.spawn() {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
warn!("failed to spawn notifier '{}': {e}", notify_command[0]);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(notify: Option<Vec<String>>) -> Self {
|
||||
Self {
|
||||
notify_command: notify,
|
||||
#[cfg(target_os = "macos")]
|
||||
native: macos::MacNotifier::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserNotifier {
|
||||
fn default() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// User can configure a program that will receive notifications. Each
|
||||
/// notification is serialized as JSON and passed as an argument to the
|
||||
/// program.
|
||||
@@ -85,3 +104,345 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use super::UserNotification;
|
||||
use crate::config;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use mac_notification_sys::Notification;
|
||||
use mac_notification_sys::NotificationResponse;
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
const HELPER_ENV: &str = "CODEX_NOTIFIER_APP";
|
||||
const HELPER_BUNDLE_NAME: &str = "CodexNotifier.app";
|
||||
const HELPER_EXECUTABLE: &str = "Contents/MacOS/codex-notifier";
|
||||
const INFO_PLIST_TEMPLATE: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../notifier/Resources/Info.plist"
|
||||
));
|
||||
const ICON_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../notifier/Resources/Codex.icns"
|
||||
));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MacNotifier {
|
||||
helper: Option<HelperApp>,
|
||||
legacy: LegacyNotifier,
|
||||
}
|
||||
|
||||
impl MacNotifier {
|
||||
pub(crate) fn new() -> Self {
|
||||
let helper = match HelperApp::discover() {
|
||||
Ok(app) => {
|
||||
debug!(
|
||||
"using Codex helper notifier at {}",
|
||||
app.bundle_path.display()
|
||||
);
|
||||
Some(app)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("no helper notifier available: {err:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
helper,
|
||||
legacy: LegacyNotifier::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify(&self, notification: &UserNotification) {
|
||||
if env::var("CODEX_SANDBOX").is_ok() {
|
||||
return;
|
||||
}
|
||||
if env::var("CI").is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(helper) = &self.helper {
|
||||
if let Err(err) = helper.notify(notification) {
|
||||
warn!("failed to send macOS notification via helper: {err:?}");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.legacy.notify(notification);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HelperApp {
|
||||
bundle_path: PathBuf,
|
||||
}
|
||||
|
||||
impl HelperApp {
|
||||
fn discover() -> Result<Self> {
|
||||
if let Ok(custom) = env::var(HELPER_ENV) {
|
||||
let candidate = PathBuf::from(custom);
|
||||
if Self::is_valid_bundle(&candidate) {
|
||||
return Ok(Self {
|
||||
bundle_path: candidate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for path in Self::candidate_paths()? {
|
||||
if Self::is_valid_bundle(&path) {
|
||||
return Ok(Self { bundle_path: path });
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(installed) = Self::install_to_codex_home() {
|
||||
if Self::is_valid_bundle(&installed) {
|
||||
return Ok(Self {
|
||||
bundle_path: installed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("helper bundle not found"))
|
||||
}
|
||||
|
||||
fn candidate_paths() -> Result<Vec<PathBuf>> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
if let Ok(home) = config::find_codex_home() {
|
||||
candidates.push(home.join(HELPER_BUNDLE_NAME));
|
||||
candidates.push(home.join("bin").join(HELPER_BUNDLE_NAME));
|
||||
}
|
||||
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
candidates.push(parent.join(HELPER_BUNDLE_NAME));
|
||||
if let Some(grand) = parent.parent() {
|
||||
candidates.push(grand.join(HELPER_BUNDLE_NAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
fn install_to_codex_home() -> Result<PathBuf> {
|
||||
let mut base = config::find_codex_home()?;
|
||||
base.push("bin");
|
||||
fs::create_dir_all(&base)?;
|
||||
let bundle_path = base.join(HELPER_BUNDLE_NAME);
|
||||
if !Self::is_valid_bundle(&bundle_path) {
|
||||
Self::write_bundle(&bundle_path)?;
|
||||
}
|
||||
Ok(bundle_path)
|
||||
}
|
||||
|
||||
fn write_bundle(destination: &Path) -> Result<()> {
|
||||
let contents = destination.join("Contents");
|
||||
let macos_dir = contents.join("MacOS");
|
||||
let resources_dir = contents.join("Resources");
|
||||
fs::create_dir_all(&macos_dir)?;
|
||||
fs::create_dir_all(&resources_dir)?;
|
||||
|
||||
fs::write(contents.join("Info.plist"), INFO_PLIST_TEMPLATE)
|
||||
.context("failed to write Info.plist")?;
|
||||
fs::write(resources_dir.join("Codex.icns"), ICON_BYTES)
|
||||
.context("failed to write Codex.icns")?;
|
||||
|
||||
let binary_src = Self::locate_binary().context("codex-notifier binary not found")?;
|
||||
let binary_dest = macos_dir.join("codex-notifier");
|
||||
fs::copy(&binary_src, &binary_dest)
|
||||
.with_context(|| format!("failed to copy notifier binary from {binary_src:?}"))?;
|
||||
|
||||
let mut perms = fs::metadata(&binary_dest)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&binary_dest, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn locate_binary() -> Result<PathBuf> {
|
||||
for candidate in Self::binary_candidate_paths()? {
|
||||
if candidate.is_file() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
if candidate.is_dir() {
|
||||
let nested = candidate.join("codex-notifier");
|
||||
if nested.is_file() {
|
||||
return Ok(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!("codex-notifier binary unavailable"))
|
||||
}
|
||||
|
||||
fn binary_candidate_paths() -> Result<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
paths.push(parent.join("codex-notifier"));
|
||||
paths.push(parent.join("../codex-notifier"));
|
||||
paths.push(parent.join("../../codex-notifier"));
|
||||
if let Some(grand) = parent.parent() {
|
||||
paths.push(grand.join("codex-notifier"));
|
||||
paths.push(grand.join("release").join("codex-notifier"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(home) = config::find_codex_home() {
|
||||
paths.push(home.join("bin").join("codex-notifier"));
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
fn is_valid_bundle(path: &Path) -> bool {
|
||||
let exec = path.join(HELPER_EXECUTABLE);
|
||||
exec.is_file()
|
||||
}
|
||||
|
||||
fn notify(&self, notification: &UserNotification) -> Result<()> {
|
||||
let payload = serde_json::to_vec(notification)?;
|
||||
|
||||
let mut temp = NamedTempFile::new().context("failed to create temp payload file")?;
|
||||
temp.as_file_mut()
|
||||
.write_all(&payload)
|
||||
.context("failed to write payload")?;
|
||||
temp.flush().ok();
|
||||
|
||||
let temp_path = temp.into_temp_path();
|
||||
let payload_path = temp_path
|
||||
.keep()
|
||||
.context("failed to persist payload file for helper app")?;
|
||||
|
||||
let status = Command::new("open")
|
||||
.arg("-n")
|
||||
.arg(&self.bundle_path)
|
||||
.arg("--args")
|
||||
.arg("--payload-file")
|
||||
.arg(&payload_path)
|
||||
.status()
|
||||
.with_context(|| {
|
||||
format!("failed to invoke open for {}", self.bundle_path.display())
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
let _ = fs::remove_file(&payload_path);
|
||||
return Err(anyhow!("open exited with status {status:?}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LegacyNotifier {
|
||||
icon_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl LegacyNotifier {
|
||||
fn new() -> Self {
|
||||
if let Err(err) = mac_notification_sys::set_application("com.openai.codex") {
|
||||
warn!("failed to register bundle id for notifications: {err}");
|
||||
}
|
||||
|
||||
let icon_path = Self::ensure_icon()
|
||||
.map_err(|err| {
|
||||
warn!("failed to prepare macOS notification icon: {err}");
|
||||
})
|
||||
.ok();
|
||||
|
||||
Self { icon_path }
|
||||
}
|
||||
|
||||
fn notify(&self, notification: &UserNotification) {
|
||||
let (title, subtitle, message) = match notification {
|
||||
UserNotification::AgentTurnComplete {
|
||||
last_assistant_message,
|
||||
input_messages,
|
||||
..
|
||||
} => {
|
||||
let title = "Codex CLI";
|
||||
let subtitle = last_assistant_message
|
||||
.as_ref()
|
||||
.map(std::string::String::as_str)
|
||||
.unwrap_or("Turn complete");
|
||||
let message = if input_messages.is_empty() {
|
||||
String::from("Agent turn finished")
|
||||
} else {
|
||||
input_messages.join(" ")
|
||||
};
|
||||
(title.to_string(), subtitle.to_string(), message)
|
||||
}
|
||||
};
|
||||
|
||||
let mut payload = Notification::new();
|
||||
payload.title(&title);
|
||||
payload.maybe_subtitle(Some(&subtitle));
|
||||
payload.message(&message);
|
||||
payload.default_sound();
|
||||
|
||||
if let Some(icon_path) = self.icon_path.as_ref().and_then(|p| p.to_str()) {
|
||||
payload.app_icon(icon_path);
|
||||
}
|
||||
|
||||
match payload.send() {
|
||||
Ok(NotificationResponse::ActionButton(action)) => {
|
||||
debug!("Codex notification action pressed: {action}");
|
||||
}
|
||||
Ok(NotificationResponse::CloseButton(label)) => {
|
||||
debug!("Codex notification dismissed via '{label}' button");
|
||||
}
|
||||
Ok(NotificationResponse::Reply(body)) => {
|
||||
debug!("Codex notification reply entered (ignored): {body}");
|
||||
}
|
||||
Ok(NotificationResponse::Click) => {
|
||||
debug!("Codex notification clicked");
|
||||
}
|
||||
Ok(NotificationResponse::None) => {}
|
||||
Err(err) => warn!("failed to deliver macOS notification via legacy path: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_icon() -> anyhow::Result<PathBuf> {
|
||||
const ICON_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/codex-notification.png"
|
||||
));
|
||||
|
||||
let mut path = config::find_codex_home()?;
|
||||
path.push("assets");
|
||||
fs::create_dir_all(&path)?;
|
||||
path.push("codex-notification.png");
|
||||
|
||||
let needs_write = match fs::read(&path) {
|
||||
Ok(existing) => existing != ICON_BYTES,
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
if needs_write {
|
||||
fs::write(&path, ICON_BYTES)?;
|
||||
let mut perms = fs::metadata(&path)?.permissions();
|
||||
perms.set_mode(0o644);
|
||||
fs::set_permissions(&path, perms)?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
codex-rs/notifier/Cargo.toml
Normal file
12
codex-rs/notifier/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "codex-notifier"
|
||||
version = { workspace = true }
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
BIN
codex-rs/notifier/Resources/Codex.icns
Normal file
BIN
codex-rs/notifier/Resources/Codex.icns
Normal file
Binary file not shown.
30
codex-rs/notifier/Resources/Info.plist
Normal file
30
codex-rs/notifier/Resources/Info.plist
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>codex-notifier</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.openai.codex.notifier</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Codex Notifier</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>14.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Codex</string>
|
||||
<key>NSUserNotificationUsageDescription</key>
|
||||
<string>Codex CLI uses notifications to tell you when a turn completes.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
21
codex-rs/notifier/build.rs
Normal file
21
codex-rs/notifier/build.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::path::PathBuf;
|
||||
|
||||
let source = PathBuf::from("src/macos/notification.mm");
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
|
||||
let mut build = cc::Build::new();
|
||||
build
|
||||
.cpp(true)
|
||||
.flag("-fobjc-arc")
|
||||
.flag("-fmodules")
|
||||
.file(&source);
|
||||
build.compile("codex_macos_notification");
|
||||
|
||||
println!("cargo:rustc-link-lib=framework=UserNotifications");
|
||||
println!("cargo:rustc-link-lib=framework=Foundation");
|
||||
println!("cargo:rustc-link-lib=framework=AppKit");
|
||||
}
|
||||
}
|
||||
153
codex-rs/notifier/src/macos/notification.mm
Normal file
153
codex-rs/notifier/src/macos/notification.mm
Normal file
@@ -0,0 +1,153 @@
|
||||
#include <AppKit/AppKit.h>
|
||||
#include <UserNotifications/UserNotifications.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@interface CodexNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
|
||||
@end
|
||||
|
||||
@implementation CodexNotificationDelegate
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
willPresentNotification:(UNNotification *)notification
|
||||
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
|
||||
completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||
withCompletionHandler:(void (^)(void))completionHandler {
|
||||
completionHandler();
|
||||
}
|
||||
@end
|
||||
|
||||
static void install_delegate(UNUserNotificationCenter *center) {
|
||||
static dispatch_once_t onceToken;
|
||||
static CodexNotificationDelegate *delegate = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
delegate = [CodexNotificationDelegate new];
|
||||
center.delegate = delegate;
|
||||
});
|
||||
}
|
||||
|
||||
static BOOL ensure_authorization(UNUserNotificationCenter *center) {
|
||||
dispatch_semaphore_t settings_wait = dispatch_semaphore_create(0);
|
||||
__block UNAuthorizationStatus status = UNAuthorizationStatusNotDetermined;
|
||||
|
||||
[center
|
||||
getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||
status = settings.authorizationStatus;
|
||||
dispatch_semaphore_signal(settings_wait);
|
||||
}];
|
||||
|
||||
dispatch_semaphore_wait(settings_wait, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (status == UNAuthorizationStatusDenied) {
|
||||
fprintf(stderr, "[codex-notifier] notification authorization currently denied\n");
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (status == UNAuthorizationStatusAuthorized ||
|
||||
status == UNAuthorizationStatusProvisional) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
dispatch_semaphore_t request_wait = dispatch_semaphore_create(0);
|
||||
__block BOOL granted = NO;
|
||||
|
||||
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound |
|
||||
UNAuthorizationOptionBadge)
|
||||
completionHandler:^(BOOL success, NSError *error) {
|
||||
granted = success;
|
||||
if (error) {
|
||||
fprintf(stderr, "[codex-notifier] authorization request error: %s\n",
|
||||
error.localizedDescription.UTF8String);
|
||||
}
|
||||
dispatch_semaphore_signal(request_wait);
|
||||
}];
|
||||
|
||||
dispatch_semaphore_wait(request_wait, DISPATCH_TIME_FOREVER);
|
||||
return granted;
|
||||
}
|
||||
|
||||
extern "C" int codex_post_user_notification(const char *title_c,
|
||||
const char *subtitle_c,
|
||||
const char *body_c,
|
||||
const char *icon_path_c) {
|
||||
setenv("__CFBundleIdentifier", "com.openai.codex.notifier", 0);
|
||||
|
||||
@autoreleasepool {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
if (!center) {
|
||||
fprintf(stderr, "[codex-notifier] UNUserNotificationCenter unavailable\n");
|
||||
return -10;
|
||||
}
|
||||
|
||||
install_delegate(center);
|
||||
|
||||
if (!ensure_authorization(center)) {
|
||||
return -11;
|
||||
}
|
||||
|
||||
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
||||
if (!content) {
|
||||
fprintf(stderr, "[codex-notifier] failed to allocate notification content\n");
|
||||
return -12;
|
||||
}
|
||||
|
||||
if (title_c && title_c[0] != '\0') {
|
||||
content.title = [NSString stringWithUTF8String:title_c];
|
||||
} else {
|
||||
content.title = @"Codex CLI";
|
||||
}
|
||||
|
||||
if (subtitle_c && subtitle_c[0] != '\0') {
|
||||
content.subtitle = [NSString stringWithUTF8String:subtitle_c];
|
||||
}
|
||||
|
||||
if (body_c && body_c[0] != '\0') {
|
||||
content.body = [NSString stringWithUTF8String:body_c];
|
||||
}
|
||||
|
||||
if (icon_path_c && icon_path_c[0] != '\0') {
|
||||
NSError *attachmentError = nil;
|
||||
NSString *path = [NSString stringWithUTF8String:icon_path_c];
|
||||
UNNotificationAttachment *attachment =
|
||||
[UNNotificationAttachment attachmentWithIdentifier:@"codex-icon"
|
||||
URL:[NSURL fileURLWithPath:path]
|
||||
options:nil
|
||||
error:&attachmentError];
|
||||
if (attachment) {
|
||||
content.attachments = @[attachment];
|
||||
} else if (attachmentError) {
|
||||
fprintf(stderr, "[codex-notifier] attachment error: %s\n",
|
||||
attachmentError.localizedDescription.UTF8String);
|
||||
}
|
||||
}
|
||||
|
||||
UNTimeIntervalNotificationTrigger *trigger =
|
||||
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO];
|
||||
NSString *identifier =
|
||||
[NSString stringWithFormat:@"com.openai.codex.notifier.%@", [[NSUUID UUID] UUIDString]];
|
||||
UNNotificationRequest *request =
|
||||
[UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
|
||||
|
||||
dispatch_semaphore_t submit_wait = dispatch_semaphore_create(0);
|
||||
__block NSError *submit_error = nil;
|
||||
[center addNotificationRequest:request
|
||||
withCompletionHandler:^(NSError *error) {
|
||||
submit_error = error;
|
||||
dispatch_semaphore_signal(submit_wait);
|
||||
}];
|
||||
|
||||
dispatch_semaphore_wait(submit_wait, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (submit_error) {
|
||||
fprintf(stderr, "[codex-notifier] addNotificationRequest error: %s\n",
|
||||
submit_error.localizedDescription.UTF8String);
|
||||
return -13;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
161
codex-rs/notifier/src/main.rs
Normal file
161
codex-rs/notifier/src/main.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
#![cfg_attr(not(target_os = "macos"), allow(unused))]
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::ffi::CString;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::io::{self};
|
||||
use std::os::raw::c_char;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
enum NotificationPayload {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
AgentTurnComplete {
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
cwd: String,
|
||||
#[serde(default)]
|
||||
input_messages: Vec<String>,
|
||||
#[serde(default)]
|
||||
last_assistant_message: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe extern "C" {
|
||||
fn codex_post_user_notification(
|
||||
title: *const c_char,
|
||||
subtitle: *const c_char,
|
||||
body: *const c_char,
|
||||
icon_path: *const c_char,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
real_main()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn real_main() -> Result<()> {
|
||||
let payload = read_payload()?;
|
||||
let (title, subtitle, body) = render_notification(&payload)?;
|
||||
dispatch_notification(&title, subtitle.as_deref(), &body)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn real_main() -> Result<()> {
|
||||
bail!("codex-notifier is only supported on macOS");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_payload() -> Result<NotificationPayload> {
|
||||
let mut args = env::args().skip(1);
|
||||
let mut payload_json: Option<String> = None;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--payload" => {
|
||||
let Some(value) = args.next() else {
|
||||
bail!("missing value for --payload");
|
||||
};
|
||||
payload_json = Some(value);
|
||||
break;
|
||||
}
|
||||
"--payload-file" => {
|
||||
let Some(path) = args.next() else {
|
||||
bail!("missing value for --payload-file");
|
||||
};
|
||||
payload_json = Some(
|
||||
fs::read_to_string(Path::new(&path))
|
||||
.with_context(|| format!("failed to read payload file at {path}"))?,
|
||||
);
|
||||
// Best-effort cleanup to avoid cluttering temp directories.
|
||||
let _ = fs::remove_file(path);
|
||||
break;
|
||||
}
|
||||
// Ignore arguments injected by `open -a`.
|
||||
arg if arg.starts_with("-Apple") || arg == "-NSDocumentRevisionsDebugMode" => {
|
||||
let _ = args.next(); // consume companion value if present
|
||||
}
|
||||
_ => {
|
||||
// Unrecognised argument – continue scanning in case the payload flag appears later.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let json = if let Some(payload) = payload_json {
|
||||
payload
|
||||
} else {
|
||||
let mut buf = String::new();
|
||||
io::stdin()
|
||||
.read_to_string(&mut buf)
|
||||
.context("failed to read payload from stdin")?;
|
||||
buf
|
||||
};
|
||||
|
||||
serde_json::from_str(&json).context("failed to parse notification payload JSON")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn render_notification(payload: &NotificationPayload) -> Result<(String, Option<String>, String)> {
|
||||
match payload {
|
||||
NotificationPayload::AgentTurnComplete {
|
||||
input_messages,
|
||||
last_assistant_message,
|
||||
cwd,
|
||||
..
|
||||
} => {
|
||||
let title = "Codex CLI".to_string();
|
||||
let subtitle = last_assistant_message
|
||||
.as_ref()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| Some("Turn complete".to_string()));
|
||||
|
||||
let mut body = String::new();
|
||||
if !input_messages.is_empty() {
|
||||
body.push_str(&input_messages.join(" "));
|
||||
}
|
||||
if body.is_empty() {
|
||||
body.push_str("Agent turn finished");
|
||||
}
|
||||
body.push_str("\n");
|
||||
body.push_str(cwd);
|
||||
|
||||
Ok((title, subtitle, body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn dispatch_notification(title: &str, subtitle: Option<&str>, body: &str) -> Result<()> {
|
||||
let title_c = CString::new(title)?;
|
||||
let subtitle_c = subtitle
|
||||
.map(|s| CString::new(s))
|
||||
.transpose()
|
||||
.context("invalid subtitle string")?;
|
||||
let body_c = CString::new(body)?;
|
||||
|
||||
let icon_ptr = std::ptr::null();
|
||||
|
||||
let code = unsafe {
|
||||
codex_post_user_notification(
|
||||
title_c.as_ptr(),
|
||||
subtitle_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
|
||||
body_c.as_ptr(),
|
||||
icon_ptr,
|
||||
)
|
||||
};
|
||||
|
||||
if code != 0 {
|
||||
bail!("codex_post_user_notification returned error code {code}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -615,6 +615,10 @@ function without the extra dependencies.
|
||||
|
||||
### notify
|
||||
|
||||
On macOS, Codex now emits native Notification Center alerts by default for supported events. The CLI packages a signed notification helper and a Codex icon, so you’ll see trusted banners without needing to install `terminal-notifier`. If you prefer to take full control, or are on another platform, you can still wire up your own script via the `notify` setting described below. Providing a `notify` command overrides the built-in macOS integration.
|
||||
|
||||
The macOS build installs a background agent bundle at `~/.codex/bin/CodexNotifier.app`. Codex writes the notification payload to a temporary JSON file and launches the agent via `open -n ... --args --payload-file`. The agent removes the file after reading it. Set `CODEX_NOTIFIER_APP` to point at a different bundle if you maintain your own build.
|
||||
|
||||
Specify a program that will be executed to get notified about events generated by Codex. Note that the program will receive the notification argument as a string of JSON, e.g.:
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user