From 5de9b5560e8135af551a6117c8d7902508df1e34 Mon Sep 17 00:00:00 2001 From: bardonadam Date: Mon, 12 Oct 2026 11:03:07 +0700 Subject: [PATCH 1/4] feat: replace a sent push notification with push --replace Every push now prints its Notification ID, and the human-readable output reads devices_notified from the SDK's snake_case response again. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +++ package.json | 2 +- skills/activitysmith/SKILL.md | 17 ++++++++++++ skills/activitysmith/scripts/send_push.sh | 17 ++++++++++-- src/cli.js | 34 ++++++++++++++++++----- test/cli.test.js | 33 ++++++++++++++++++++++ 6 files changed, 97 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e490a..83291b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.14.0 + +Adds `push --replace ` to silently replace a Push Notification you already sent. The replacement takes the old notification's place on the Lock Screen and in history. Every push now prints its Notification ID, and the human-readable output shows the number of devices notified again. + ## 1.13.0 Adds custom icons and interruption levels for Push Notifications. Set `--icon` to show a service logo or avatar on the notification, and `--interruption-level` to deliver it as `passive`, `active`, or `time-sensitive`. diff --git a/package.json b/package.json index 4ee31d1..a02aeb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "activitysmith-cli", - "version": "1.13.0", + "version": "1.14.0", "description": "Command-line interface for ActivitySmith. Send Push Notifications, start, update, and end Live Activities, and set App Icon Badge Counts from your terminal.", "keywords": [ "activitysmith", diff --git a/skills/activitysmith/SKILL.md b/skills/activitysmith/SKILL.md index bbd53db..150c247 100644 --- a/skills/activitysmith/SKILL.md +++ b/skills/activitysmith/SKILL.md @@ -34,6 +34,7 @@ export ACTIVITYSMITH_API_KEY="..." ## Pick The Right Signal - Push Notification: one important event, completion, blocker, review request, or handoff. +- Push Notification replacement: an earlier Push Notification is out of date and should be swapped, not stacked. - Push Notification with redirection: tapping the notification should open a URL or run a specific Shortcut that already exists on the user's iPhone. - Push Notification with actions: long-press should show up to 4 buttons. - Live Activity: a real-time metric, operational state, or ongoing task should stay visible. @@ -65,6 +66,22 @@ Use this when the user says "notify me when done" or when a long task completes. -m "Your agent finished the task." ``` +### Replace A Notification Once It Is Handled + +Use this when an earlier Push Notification is out of date, for example the user already answered from their computer. Every push prints a `Notification ID`. Pass it with `-R` to swap the old notification for the new one on the Lock Screen and in history. The replacement is silent and goes to the same recipients. + +```bash +./skills/activitysmith/scripts/send_push.sh \ + -t "Input needed" \ + -m "Approve the migration to continue." +# Output includes: Notification ID: Xk3pQ8mV2nR7tL9wB4yZ + +./skills/activitysmith/scripts/send_push.sh \ + -R "Xk3pQ8mV2nR7tL9wB4yZ" \ + -t "Answered from desk" \ + -m "Migration approved. Nothing needed here." +``` + ### Notify With A Shortcut Tap Target Use this when tapping the notification should run a specific Shortcut that already exists on the user's iPhone. diff --git a/skills/activitysmith/scripts/send_push.sh b/skills/activitysmith/scripts/send_push.sh index bfd436d..ff685b4 100755 --- a/skills/activitysmith/scripts/send_push.sh +++ b/skills/activitysmith/scripts/send_push.sh @@ -3,7 +3,7 @@ set -euo pipefail usage() { cat <<'USAGE' -Usage: send_push.sh -m "message" [-t "title"] [-s "subtitle"] [-c "channels"] [-g "tags"] [-M "media"] [-r "redirection"] [-a "actions-json"] [-A "actions-file"] +Usage: send_push.sh -m "message" [-t "title"] [-s "subtitle"] [-c "channels"] [-g "tags"] [-M "media"] [-r "redirection"] [-a "actions-json"] [-A "actions-file"] [-R "notification-id"] Required: -m Push message @@ -17,6 +17,8 @@ Optional: -r Redirection HTTPS or shortcuts:// URL (opened on notification tap) -a Actions JSON array string (max 4 actions) -A Path to actions JSON array file + -R Notification ID to silently replace instead of sending a new push + (cannot be combined with -c) USAGE } @@ -29,8 +31,9 @@ media="" redirection="" actions_json="" actions_file="" +replace_id="" -while getopts ":m:t:s:c:g:M:r:a:A:h" opt; do +while getopts ":m:t:s:c:g:M:r:a:A:R:h" opt; do case "$opt" in m) message="$OPTARG" ;; t) title="$OPTARG" ;; @@ -41,6 +44,7 @@ while getopts ":m:t:s:c:g:M:r:a:A:h" opt; do r) redirection="$OPTARG" ;; a) actions_json="$OPTARG" ;; A) actions_file="$OPTARG" ;; + R) replace_id="$OPTARG" ;; h) usage; exit 0 ;; \?) echo "Unknown option: -$OPTARG"; usage; exit 1 ;; :) echo "Missing value for -$OPTARG"; usage; exit 1 ;; @@ -65,6 +69,12 @@ if [[ -n "$media" && ( -n "$actions_json" || -n "$actions_file" ) ]]; then exit 1 fi +if [[ -n "$replace_id" && -n "$channels" ]]; then + echo "Replacements go to the original recipients; -c cannot be combined with -R." + usage + exit 1 +fi + # shellcheck disable=SC1091 source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" load_env_if_present @@ -72,6 +82,9 @@ require_api_key require_cli cmd=(push --title "$title" --message "$message") +if [[ -n "$replace_id" ]]; then + cmd+=(--replace "$replace_id") +fi if [[ -n "$subtitle" ]]; then cmd+=(--subtitle "$subtitle") fi diff --git a/src/cli.js b/src/cli.js index 93af172..39f6f20 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1257,8 +1257,12 @@ const outputResult = (payload, options, lines) => { program .command("push") - .description("Send a push notification") + .description("Send a push notification, or replace one you already sent") .requiredOption("--title ", "Push title") + .option( + "--replace <notification-id>", + "Silently replace a sent push notification, using the notification ID from its send output" + ) .option("--message <message>", "Push message") .option("--subtitle <subtitle>", "Push subtitle") .option( @@ -1299,6 +1303,16 @@ program media: options.media, actions, }); + if (options.replace !== undefined) { + if (options.channels !== undefined || options.interruptionLevel !== undefined) { + throw new Error( + "--channels and --interruption-level cannot be combined with --replace. Replacements go to the original recipients and arrive silently." + ); + } + if (options.replace.trim() === "") { + throw new Error("--replace requires the notification ID from the original push output."); + } + } const pushNotificationRequest = withTargetChannels( { @@ -1325,18 +1339,24 @@ program options.channels ); - const response = await client.notifications.sendPushNotification({ - pushNotificationRequest, - }); + const isReplacement = options.replace !== undefined; + const response = isReplacement + ? await client.notifications.replace(options.replace.trim(), pushNotificationRequest) + : await client.notifications.sendPushNotification({ + pushNotificationRequest, + }); + const devicesNotified = response?.devices_notified ?? response?.devicesNotified; + const notificationId = response?.notification_id ?? response?.notificationId; outputResult(response, globalOptions, [ - "Push sent.", + isReplacement ? "Push replaced." : "Push sent.", response?.success !== undefined ? `Success: ${response.success}` : null, - response?.devicesNotified !== undefined - ? `Devices notified: ${response.devicesNotified}` + devicesNotified !== undefined + ? `Devices notified: ${devicesNotified}` : null, + notificationId ? `Notification ID: ${notificationId}` : null, ]); } catch (error) { await handleError(error, globalOptions); diff --git a/test/cli.test.js b/test/cli.test.js index 296dcb3..c602520 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -8,11 +8,14 @@ const runCli = (args) => globalThis.fetch = async (url, init) => { process.stdout.write("CAPTURE:" + JSON.stringify({ url, + method: init.method, body: init.body ? JSON.parse(init.body) : null }) + "\\n"); return new Response(JSON.stringify({ success: true, activity_id: "activity-1", + notification_id: "pn-1", + devices_notified: 1, operation: "started" }), { status: 200, @@ -356,3 +359,33 @@ test('push rejects invalid interruption levels and non-HTTPS icons before transp assert.equal(result.request, null); } }); + +test("push --replace silently replaces a sent push notification", async () => { + const result = await runCli([ + "push", + "--replace", + "pn-1", + "--title", + "Answered from desk", + "--message", + "No input needed", + ]); + + assert.equal(result.code, 0, result.stderr); + assert.equal(result.request.method, "PUT"); + assert.match(result.request.url, /\/push-notification\/pn-1$/); + assert.deepEqual(result.request.body, { + title: "Answered from desk", + message: "No input needed", + }); +}); + +test("push --replace rejects options that only apply to new sends", async () => { + for (const extra of [["--channels", "ops"], ["--interruption-level", "passive"]]) { + const result = await runCli(["push", "--replace", "pn-1", "--title", "Done", ...extra]); + + assert.notEqual(result.code, 0); + assert.match(result.stdout + result.stderr, /cannot be combined with --replace/); + assert.equal(result.request, null); + } +}); From 92ab9ab49f8c04d8d2d739eb2eb8b4b7143d8c03 Mon Sep 17 00:00:00 2001 From: bardonadam <adam@initsignal.com> Date: Mon, 12 Oct 2026 11:13:07 +0700 Subject: [PATCH 2/4] docs: use an incident example in the skill's replace recipe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- skills/activitysmith/SKILL.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skills/activitysmith/SKILL.md b/skills/activitysmith/SKILL.md index 150c247..484e951 100644 --- a/skills/activitysmith/SKILL.md +++ b/skills/activitysmith/SKILL.md @@ -66,20 +66,20 @@ Use this when the user says "notify me when done" or when a long task completes. -m "Your agent finished the task." ``` -### Replace A Notification Once It Is Handled +### Replace A Notification When The Status Changes -Use this when an earlier Push Notification is out of date, for example the user already answered from their computer. Every push prints a `Notification ID`. Pass it with `-R` to swap the old notification for the new one on the Lock Screen and in history. The replacement is silent and goes to the same recipients. +Use this to update a Push Notification you already sent, for example when an incident you alerted on is resolved. Every push prints a `Notification ID`. Pass it with `-R` to swap the old notification for the new one on the Lock Screen and in history. The replacement is silent and goes to the same recipients. ```bash ./skills/activitysmith/scripts/send_push.sh \ - -t "Input needed" \ - -m "Approve the migration to continue." + -t "🚨 checkout-api is down" \ + -m "500 errors on 62% of requests." # Output includes: Notification ID: Xk3pQ8mV2nR7tL9wB4yZ ./skills/activitysmith/scripts/send_push.sh \ -R "Xk3pQ8mV2nR7tL9wB4yZ" \ - -t "Answered from desk" \ - -m "Migration approved. Nothing needed here." + -t "✅ checkout-api recovered" \ + -m "Error rate is back to normal. Incident resolved." ``` ### Notify With A Shortcut Tap Target From 72d7ef811f75f15bc0b7ed70c3ecfb5322da6b6f Mon Sep 17 00:00:00 2001 From: bardonadam <adam@initsignal.com> Date: Mon, 12 Oct 2026 11:32:19 +0700 Subject: [PATCH 3/4] docs: add the 1.14.0 changelog and replace docs to the README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- CHANGELOG.md | 2 +- README.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83291b8..3f9bde9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 1.14.0 -Adds `push --replace <notification-id>` to silently replace a Push Notification you already sent. The replacement takes the old notification's place on the Lock Screen and in history. Every push now prints its Notification ID, and the human-readable output shows the number of devices notified again. +Adds the ability to replace a Push Notification you already sent with a new one. ## 1.13.0 diff --git a/README.md b/README.md index 6220cd1..fb36ce8 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,22 @@ activitysmith push \ --interruption-level "time-sensitive" ``` +### Replace a Push Notification + +Every `push` prints a `Notification ID`. Pass it to `--replace` to update a Push Notification you already sent, for example when an incident you alerted on is resolved. The replacement takes the old notification's place on the Lock Screen and in history instead of stacking a second one. It arrives silently, without sound or waking the screen, and goes to the same recipients as the original, so `--channels` and `--interruption-level` don't apply. + +```bash +activitysmith push \ + --title "🚨 checkout-api is down" \ + --message "500 errors on 62% of requests." +# Notification ID: Xk3pQ8mV2nR7tL9wB4yZ + +activitysmith push \ + --replace "Xk3pQ8mV2nR7tL9wB4yZ" \ + --title "✅ checkout-api recovered" \ + --message "Error rate is back to normal. Incident resolved." +``` + ## Live Activities Choose the Live Activity type that matches what you want to show: From b00da22c17554ee43228d6332197fbf12cd641a1 Mon Sep 17 00:00:00 2001 From: bardonadam <adam@initsignal.com> Date: Mon, 12 Oct 2026 11:35:13 +0700 Subject: [PATCH 4/4] chore: require activitysmith 1.14.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a02aeb6..98aaf80 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "test": "node --test" }, "dependencies": { - "activitysmith": "^1.13.0", + "activitysmith": "^1.14.0", "commander": "^12.1.0" } }