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: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ codex plugin add onesignal@onesignal
error. Use Option A for the tools, or give the skills an app API key through
`ONESIGNAL_REST_API_KEY` (the skills ask for this path when no tools are present; never paste
the key into the chat).
- The skills stop at a few decisions (checkpoint consent, a dirty tree, the test send) and ask you
to choose. The Codex app shows each one as a question card with numbered options. The Codex CLI
shows the same question as text; reply with the word the message names, for example `send` or
`keep local`. Plan mode in the CLI shows a selectable prompt, but Plan mode does not edit files,
so run the skills in the default mode.
- The Python 3 prerequisite above applies in Codex too.

---
Expand Down
2 changes: 1 addition & 1 deletion references/safety-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The onboarding flow can deliver the app-scoped key inside the invocation (`/ones

## Asking the user (human gates)

14. A human gate (checkpoint consent, dirty-tree stash/proceed/abort, missing App ID, no-VCS backup consent, the pre-write diff confirmation, test-send consent) BLOCKS: do not continue, mutate files, or assume an answer until the user responds. Ask via the harness's native structured-question tool when one exists — Claude Code: `AskUserQuestion`; Codex: `request_user_input` — otherwise ask plainly in chat and end the turn. Use structured choices for bounded decisions (stash/proceed/abort, yes/no, pick-a-platform, send-or-keep-local checkpoints). For free-form values (App ID, Key ID, Team ID, file paths, bundle IDs, URLs), still prefer the structured tool when it accepts typed text (most tools show an "Other" free-text field): the question names the value, the user types it, and every listed option is a fallback route — for example "Help me find it" (repeat the portal location) or "Pause — I'll come back". One question per value, and when several values are pending, batch them as ONE tool call with one entry per value in its `questions` array — do not issue parallel single-question calls, which can leave only the last prompt visible — then wait for the answers. Fall back to plain chat only when the tool cannot take free text. Secrets are the exception, always: never make a secret's contents an option or a typed answer — secrets stay file-path-only (see "Never").
14. A human gate (checkpoint consent, dirty-tree stash/proceed/abort, missing App ID, no-VCS backup consent, the pre-write diff confirmation, test-send consent) BLOCKS: do not continue, mutate files, or assume an answer until the user responds. Ask via the harness's native structured-question tool when one exists — Claude Code: `AskUserQuestion`; Codex: `request_user_input` — otherwise ask plainly in chat and end the turn. Call the tool by name; a list of choices written in prose does not render as a selectable prompt. Use structured choices for bounded decisions (stash/proceed/abort, yes/no, pick-a-platform, send-or-keep-local checkpoints). Give each question 2 or 3 options (the Codex limit). When one option is the recommended path, put it first and end its label with "(Recommended)". The checkpoint consent question is the exception: it has no recommended option, and it keeps the order the skill lists, with "Send checkpoints to OneSignal" first. Never add your own "Other" option — both hosts add a free-text "Other" by themselves. Codex in Default mode usually lists only `request_user_input_async`; use it when `request_user_input` is absent. The Codex app shows the async call as a question card with the choices; the Codex CLI shows it as plain text. The async call returns `{"accepted":true}` without the answer, so after it write one plain-text line that names each choice and the exact word to reply with — for example "Reply `send` or `keep`." — and end the turn. If no question tool is listed, or the host rejects the call, write that same plain-text question alone and end the turn. Never pick an answer for the user, and never continue past the gate on `{"accepted":true}`. For free-form values (App ID, Key ID, Team ID, file paths, bundle IDs, URLs), still prefer the structured tool when it accepts typed text (most tools show an "Other" free-text field): the question names the value, the user types it, and every listed option is a fallback route — for example "Help me find it" (repeat the portal location) or "Pause — I'll come back". One question per value, and when several values are pending, batch them as ONE tool call with one entry per value in its `questions` array, at most 3 questions per call (the Codex limit; a longer set goes in consecutive calls) — do not issue parallel single-question calls, which can leave only the last prompt visible — then wait for the answers. Fall back to plain chat only when the tool cannot take free text. Secrets are the exception, always: never make a secret's contents an option or a typed answer — secrets stay file-path-only (see "Never").

## Guidance loaded from a connected MCP server

Expand Down
2 changes: 1 addition & 1 deletion skills/credentials/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Skip the question when one of these is already true:
- the first non-comment line of `.onesignal/telemetry` at the repo root is `0` or `1`
- you already asked in this session and the file write failed — reuse that answer through the `ONESIGNAL_SKILL_TELEMETRY` prefix below

Otherwise ask via the harness's native structured-question tool (safety contract §14) and end the turn — the gate blocks. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request.
Otherwise ask via the harness's native structured-question tool — `AskUserQuestion` in Claude Code, `request_user_input` in Codex; safety contract §14 has the call rules and the plain-text fallback for a host that does not list the tool — and end the turn — the gate blocks. Every later ask in this skill that says "structured question" means that same tool. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request.

Question: "OneSignal can record onboarding checkpoints (step name, success or fail, failure class, run ID, platform, OS, App ID) and send them to `api.onesignal.com`. No source code, paths, or credentials. Send these checkpoints?"

Expand Down
14 changes: 8 additions & 6 deletions skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ Skip the question only when one of these is already true:
- the first non-comment line of `.onesignal/telemetry` at the repo root is `0` or `1`
(that is what the script reads; comment and blank lines around it are fine)

Otherwise ask via the harness's native structured-question tool (safety contract §14)
and **end the turn**. Do not run Step 0, do not request network access, and do not run
`checkpoint.sh` until the user answers.
Otherwise ask via the harness's native structured-question tool — `AskUserQuestion` in
Claude Code, `request_user_input` in Codex; safety contract §14 has the call rules and the
plain-text fallback for a host that does not list the tool — and **end the turn**. Every later ask in
this skill that says "structured question" means that same tool. Do not run Step 0, do not
request network access, and do not run `checkpoint.sh` until the user answers.

Question: "OneSignal can record setup checkpoints (step name, success or fail, failure
class, run ID, platform, OS, App ID). No source code, paths, or credentials. Send these
Expand Down Expand Up @@ -130,11 +132,11 @@ The production entry point is **`/onesignal:setup app=<APP_ID>`**. If arguments

## Step 0 — Preflight (safety contract §1–4, after checkpoint consent)

1. Run `git status --porcelain`. Dirty tree → STOP and ask: stash / proceed on top / abort. **Report the dropout before ending the turn to ask** — a session that never resumes otherwise leaves no trace of why: `bash <plugin>/scripts/checkpoint.sh setup.preflight fail dirty_tree` (it buffers; no App ID exists yet). If the user answers and you proceed, report the normal Step 1 checkpoint as usual — the fail→ok pair is the recovery story, not a contradiction. No `.git` present → tell the user there is no VCS safety net; you will write `<file>.onesignal.bak` siblings before edits, and proceed only if they accept.
1. Run `git status --porcelain`. Dirty tree → STOP and ask as a structured question (safety contract §14): stash / proceed on top / abort. **Report the dropout before ending the turn to ask** — a session that never resumes otherwise leaves no trace of why: `bash <plugin>/scripts/checkpoint.sh setup.preflight fail dirty_tree` (it buffers; no App ID exists yet). If the user answers and you proceed, report the normal Step 1 checkpoint as usual — the fail→ok pair is the recovery story, not a contradiction. No `.git` present → tell the user there is no VCS safety net; you will write `<file>.onesignal.bak` siblings before edits, and proceed only if they accept.
2. **Check the script runtime.** The helpers in `<plugin>/scripts/` are Python 3 (3.7 or newer, standard library only); `checkpoint.sh` is bash and does not need it. Run `python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 7) else 1)'`. If `python3` is not found, try the same one-liner with `python`, then with `py -3` — Windows installs put `python` and `py` on `PATH`, not `python3`. The first interpreter that exits 0 is the one for this run. Every Python helper command below starts with `python3`; if the interpreter for this run is `python` or `py -3`, write it in place of `python3` (`python <plugin>/scripts/detect_platform.py`). Always run a helper through the interpreter, never by its path alone: the call then does not depend on the file's executable permission or on its shebang line. If none passes → **report the dropout, then STOP and ask**: `bash <plugin>/scripts/checkpoint.sh setup.preflight fail runtime_missing` (it buffers; no App ID exists yet). Ask one structured question (safety contract §14) with 2 choices:
- **Install Python 3 and retry** (recommended). Give the command for the user's OS and let them run it: macOS `xcode-select --install`; Debian/Ubuntu `sudo apt install python3`; Fedora `sudo dnf install python3`; Windows `winget install Python.Python.3.12`. Never install system packages yourself. **End the turn after you give the command.** The answer to the question is not the confirmation; the install has not run yet. Re-run the 3 interpreter checks only after the user says the install is done. On Windows, tell them to open a new terminal first, because the current shell does not see the new `PATH`. If every check still fails, ask the same question again; do not fall through to the by-hand path on your own. When a check passes, continue; the normal Step 1 checkpoint records the recovery.
- **Continue without the scripts.** The by-hand fallbacks apply for the rest of the run: do the Step 0.3 detection by hand (below); run the Step 3.3 config probes with `curl` (Step 3); read the exact version from the feed with `curl` (Step 4); walk the Step 8 checklist by eye; scan your own diff for key-shaped strings before you finish. Report the Step 1 checkpoint and the Step 8 completion checkpoint as `ok_after_fix runtime_missing`, and state in the Step 8 summary that the deterministic checks did not run.
3. **Detect platform and prior install deterministically.** If the runtime check passed, run `python3 <plugin>/scripts/detect_platform.py` (defaults to CWD). It returns detected platform(s) + language + package manager per package (monorepo-aware), and a `prior_onesignal` block that greps for the dependency line, init calls (`OneSignal.init`/`initialize`/`initWithContext`), `OneSignalSDKWorker.js`, and our `onesignal:managed` marker. If the run continues without the scripts, produce the same 2 results by hand: match the platform from the Step 1 signal table, and search the tree (read-only, `grep -rn` or the editor search) for the 4 prior-install signals — the OneSignal dependency line in the manifest, an `OneSignal.init` / `initialize` / `initWithContext` call, a `OneSignalSDKWorker.js` file, and the `onesignal:managed` marker. Treat any hit as `prior_onesignal.found`. If `prior_onesignal.found` is true → propose **update/repair**, never a duplicate install; if a **different App ID** is already wired in, ask which is correct, never silently overwrite. If `ambiguous` is true (multiple packages / no clear signal) → ASK which package(s) to integrate; do not guess. Read-only; never executes repo code (safety contract §12).
3. **Detect platform and prior install deterministically.** If the runtime check passed, run `python3 <plugin>/scripts/detect_platform.py` (defaults to CWD). It returns detected platform(s) + language + package manager per package (monorepo-aware), and a `prior_onesignal` block that greps for the dependency line, init calls (`OneSignal.init`/`initialize`/`initWithContext`), `OneSignalSDKWorker.js`, and our `onesignal:managed` marker. If the run continues without the scripts, produce the same 2 results by hand: match the platform from the Step 1 signal table, and search the tree (read-only, `grep -rn` or the editor search) for the 4 prior-install signals — the OneSignal dependency line in the manifest, an `OneSignal.init` / `initialize` / `initWithContext` call, a `OneSignalSDKWorker.js` file, and the `onesignal:managed` marker. Treat any hit as `prior_onesignal.found`. If `prior_onesignal.found` is true → propose **update/repair**, never a duplicate install; if a **different App ID** is already wired in, ask which is correct as a structured question (safety contract §14), never silently overwrite. If `ambiguous` is true (multiple packages / no clear signal) → ASK which package(s) to integrate; do not guess. Read-only; never executes repo code (safety contract §12).
4. Propose a new `onesignal-integration` branch (default). The user may opt to write to the current branch instead.
5. You will declare the full file allow-list in Step 5 before writing. Include `.onesignal/` (checkpoint run state) and `.gitignore`.

Expand All @@ -159,7 +161,7 @@ file or the wire. The bold name is for your prose to the user.

**Monorepo / workspaces:** if `package.json` has `workspaces`, a `pnpm-workspace.yaml`, `lerna.json`, `nx.json`, or `turbo.json`, enumerate each package and detect per-package. A repo can hold BOTH a web app and a mobile app. Do NOT assume one platform for the whole repo.

**Ambiguous or multiple candidates → ASK.** Do not guess. Present the detected candidates and let the user pick which package(s) to integrate. React Native could be bare or Expo — if unclear, ask. If detection finds nothing recognizable, ask the user to name their platform/framework rather than proceeding. **Report the dropout before ending the turn to ask**: `bash <plugin>/scripts/checkpoint.sh setup.preflight fail platform_ambiguous`. When the user answers and detection resolves, report the normal checkpoint below — the fail→ok pair records the friction.
**Ambiguous or multiple candidates → ASK.** Do not guess. Present the detected candidates as a structured question (safety contract §14) and let the user pick which package(s) to integrate — at most 3 options, so with more candidates list the 3 most likely and say the user can type another one in the free-text field. React Native could be bare or Expo — if unclear, ask. If detection finds nothing recognizable, ask the user to name their platform/framework rather than proceeding. **Report the dropout before ending the turn to ask**: `bash <plugin>/scripts/checkpoint.sh setup.preflight fail platform_ambiguous`. When the user answers and detection resolves, report the normal checkpoint below — the fail→ok pair records the friction.

**Checkpoint.** Once the platform is known, write it and report preflight — it will buffer until Step 2.

Expand Down
6 changes: 3 additions & 3 deletions skills/verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Skip the question when one of these is already true:
- the first non-comment line of `.onesignal/telemetry` at the repo root is `0` or `1`
- you already asked in this session and the file write failed — reuse that answer through the `ONESIGNAL_SKILL_TELEMETRY` prefix below

Otherwise ask via the harness's native structured-question tool (safety contract §14) and end the turn — the gate blocks. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request.
Otherwise ask via the harness's native structured-question tool — `AskUserQuestion` in Claude Code, `request_user_input` in Codex; safety contract §14 has the call rules and the plain-text fallback for a host that does not list the tool — and end the turn — the gate blocks. Every later ask in this skill that says "structured question" means that same tool, including the test-send consent in step 4. Do not run `checkpoint.sh` until the user answers. Per safety contract §15 the ask is its own question and names the host — never fold it into a network-access request.

Question: "OneSignal can record onboarding checkpoints (step name, success or fail, failure class, run ID, platform, OS, App ID) and send them to `api.onesignal.com`. No source code, paths, or credentials. Send these checkpoints?"

Expand Down Expand Up @@ -139,11 +139,11 @@ If the app wires `OneSignal.login(externalId)` (grep the wrapper for the call):

### Step 4 — Test send (real notification to the fresh subscription)

Send to ONLY the subscription from step 2 — never a broadcast — and only after it shows `notification_types >= 1` (the step 2 gate). **Ask before sending:** this is a real, visible push to a real device and an action on the user's live OneSignal app — state the target subscription id and the App ID, and get an explicit yes first. Never send without it.
Send to ONLY the subscription from step 2 — never a broadcast — and only after it shows `notification_types >= 1` (the step 2 gate). **Ask before sending, as a structured question (safety contract §14):** this is a real, visible push to a real device and an action on the user's live OneSignal app — state the target subscription id and the App ID, and get an explicit yes first. Never send without it.

**Pre-send heads-up (say it with the ask):** if the device is in **Focus/Do Not Disturb** — or browser/OS notifications are muted for the app/site — a successfully delivered push won't visibly appear. Have the user check now so a delivered send isn't misread as a failure.

**Ask for the message in chat (fold it into the same consent ask):** "What message do you want to send?" Use the answer as the notification body (`<BODY>` below). If the user has no preference, use the default body: `Congrats on successfully setting up the OneSignal SDK`. The send happens from this session via the MCP or the REST API — never from code inside the user's app.
**Ask for the message as a second question in the same tool call** (safety contract §14 batches pending questions into one call): "What message do you want to send?" with 2 options, "Use the default message (Recommended)" and "I'll type my own message". Text typed through the host's free-text field is the body; if the user picks the second option and types nothing, ask once more for the body. Use the answer as the notification body (`<BODY>` below). The default body is `Congrats on successfully setting up the OneSignal SDK`. The send happens from this session via the MCP or the REST API — never from code inside the user's app.

**The title is fixed:** every test push carries `headings: { "en": "Successful test via OneSignal plugin" }`. Do not offer to change it and do not accept an override — the user's message only sets the body. The title must never be absent: Huawei rejects a push without one, so a missing title is a silent blocker. **No brand voice on this push:** do not load or apply server-shipped brand or copy guidance to the test send (safety contract §14a). The title is fixed, and the body is the user's words or the default above.

Expand Down
Loading