Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "OneSignal",
"shortDescription": "Set up and send push notifications",
"longDescription": "OneSignal is the market leading customer engagement solution for email, push notifications, SMS, in-app messaging, and Live Activities. This plugin brings OneSignal onboarding into Codex: point it at your codebase and it installs the SDK, sets up your push credentials, and verifies that a real notification reaches your device.\n\nOnce installed, describe what you want in plain language or invoke the plugin directly with @OneSignal.\n\nTry prompts like:\n@OneSignal send an email to promote our new product announcement.\n@OneSignal create an email template for our weekly newsletter.\n@OneSignal send an SMS to remind customers about their abandoned cart.\n@OneSignal text a discount code to my subscribers for this weekend's sale.\n@OneSignal set up push notifications in this app.\n@OneSignal upload my APNs .p8 key and Firebase service account to OneSignal.\n@OneSignal why isn't my push notification arriving on Android?\n@OneSignal send a test push to my device and confirm delivery.\n\nRequirements: A free OneSignal account. A device, simulator, or browser that can receive push notifications.",
"shortDescription": "Set up email, push, and SMS",
"longDescription": "OneSignal is a customer engagement platform for email, push notifications, SMS, in-app messaging, and Live Activities. This plugin brings OneSignal onboarding into Codex: point it at your codebase and it installs the SDK, sets up your push credentials, and verifies that a real notification reaches your device.\n\nOnce installed, describe what you want in plain language or invoke the plugin directly with @OneSignal.\n\nTry prompts like:\n@OneSignal send an email to promote our new product announcement.\n@OneSignal create an email template for our weekly newsletter.\n@OneSignal send an SMS to remind customers about their abandoned cart.\n@OneSignal text a discount code to my subscribers for this weekend's sale.\n@OneSignal set up push notifications in this app.\n@OneSignal upload my APNs .p8 key and Firebase service account to OneSignal.\n@OneSignal why isn't my push notification arriving on Android?\n@OneSignal send a test push to my device and confirm delivery.\n\nRequirements: A OneSignal account. A device, simulator, or browser that can receive push notifications.",
"developerName": "OneSignal",
"category": "Developer Tools",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://documentation.onesignal.com",
"supportURL": "https://github.com/OneSignal/onesignal-agent-plugin/issues",
"privacyPolicyURL": "https://onesignal.com/privacy_policy",
"termsOfServiceURL": "https://onesignal.com/tos",
"defaultPrompt": [
Expand Down
32 changes: 32 additions & 0 deletions scripts/package_directory_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
# The portal accepts this field only for a plugin with a custom UI
# (`screenshot_configuration_excluded`); this plugin has none.
EXCLUDED_INTERFACE_FIELDS = ("screenshots",)
# Limits the portal applies at submission, which are tighter than the ones at upload.
SUBMISSION_TEXT_LIMITS = {"displayName": 30, "shortDescription": 30}
# A plugin with an MCP connection must list all 4 URLs before submission.
LISTING_URL_FIELDS = ("websiteURL", "supportURL", "privacyPolicyURL", "termsOfServiceURL")

VERSION_FILES = {
"claude": ".claude-plugin/plugin.json",
Expand Down Expand Up @@ -257,6 +261,16 @@ def check_manifest(source, findings):
if not isinstance(interface, dict):
findings.append("%s: interface is missing" % CODEX_MANIFEST)
return
for field, limit in SUBMISSION_TEXT_LIMITS.items():
value = interface.get(field)
if not isinstance(value, str) or not value:
findings.append("%s: interface.%s is missing" % (CODEX_MANIFEST, field))
elif len(value) > limit:
findings.append("%s: interface.%s must be %d characters or fewer (got %d)" % (CODEX_MANIFEST, field, limit, len(value)))
for field in LISTING_URL_FIELDS:
value = interface.get(field)
if not isinstance(value, str) or not value.startswith("https://"):
findings.append("%s: interface.%s must be an HTTPS URL (got %r)" % (CODEX_MANIFEST, field, value))
for field in brand_asset_fields(interface):
path = interface.get(field)
if not isinstance(path, str) or not path.startswith("./%s/" % ASSETS_DIR):
Expand Down Expand Up @@ -650,6 +664,24 @@ def self_test(source):
ok = expect_failure("display name in the manifest name field", bad, os.path.join(tmp, "bad-plugin-name-work"),
"name must be lowercase letters, digits, and single hyphens") and ok

bad = os.path.join(tmp, "bad-long-subtitle")
copy_source_subset(source, bad)
path = os.path.join(bad, CODEX_MANIFEST)
manifest = json.loads(read_text(path))
manifest["interface"]["shortDescription"] = "Set up and send push, email, and SMS"
write_text(path, json.dumps(manifest, indent=2) + "\n")
ok = expect_failure("subtitle over the submission limit", bad, os.path.join(tmp, "bad-long-subtitle-work"),
"interface.shortDescription must be 30 characters or fewer") and ok

bad = os.path.join(tmp, "bad-missing-support-url")
copy_source_subset(source, bad)
path = os.path.join(bad, CODEX_MANIFEST)
manifest = json.loads(read_text(path))
del manifest["interface"]["supportURL"]
write_text(path, json.dumps(manifest, indent=2) + "\n")
ok = expect_failure("manifest without a support URL", bad, os.path.join(tmp, "bad-missing-support-url-work"),
"interface.supportURL must be an HTTPS URL") and ok

bad = os.path.join(tmp, "bad-missing-mcp-config")
copy_source_subset(source, bad)
os.remove(os.path.join(bad, MCP_CONFIG))
Expand Down
Loading