activitysmith-cli
Advanced tools
+2
-2
| { | ||
| "name": "activitysmith-cli", | ||
| "version": "1.1.0", | ||
| "version": "1.3.0", | ||
| "description": "Command-line interface for ActivitySmith. Send push notifications and manage Live Activities from your terminal.", | ||
@@ -43,5 +43,5 @@ "keywords": [ | ||
| "dependencies": { | ||
| "activitysmith": "^1.1.0", | ||
| "activitysmith": "^1.3.0", | ||
| "commander": "^12.1.0" | ||
| } | ||
| } |
+184
-35
@@ -5,2 +5,25 @@ # ActivitySmith CLI | ||
| ## Table of Contents | ||
| - [Install](#install) | ||
| - [Install Skill (Codex/Claude/Other Skills-Compatible Agents)](#install-skill-codexclaudeother-skills-compatible-agents) | ||
| - [Auth](#auth) | ||
| - [Push Notifications](#push-notifications) | ||
| - [Send Push Notification](#send-push-notification) | ||
| - [Rich Push Notifications with Media](#rich-push-notifications-with-media) | ||
| - [Actionable Push Notifications](#actionable-push-notifications) | ||
| - [Live Activities](#live-activities) | ||
| - [Simple: Let ActivitySmith manage the Live Activity for you](#simple-let-activitysmith-manage-the-live-activity-for-you) | ||
| - [Advanced: Full lifecycle control](#advanced-full-lifecycle-control) | ||
| - [Stats](#stats) | ||
| - [Metrics](#metrics) | ||
| - [Segmented Progress](#segmented-progress) | ||
| - [Progress](#progress) | ||
| - [Live Activity Action](#live-activity-action) | ||
| - [Channels](#channels) | ||
| - [Widgets](#widgets) | ||
| - [Aliases](#aliases) | ||
| - [Content State Options](#content-state-options) | ||
| - [Output](#output) | ||
| ## Install | ||
@@ -121,25 +144,28 @@ | ||
| <p align="center"> | ||
| <img src="https://cdn.activitysmith.com/features/metrics-live-activity-action.png" alt="Live Activities example" width="680" /> | ||
| <img src="https://cdn.activitysmith.com/features/metrics-live-activity-action.png" alt="Metrics Live Activity screenshot" width="680" /> | ||
| </p> | ||
| ActivitySmith supports two ways to drive Live Activities: | ||
| There are four types of Live Activities: | ||
| - Recommended: stream updates with `activitysmith activity stream ...` | ||
| - Advanced: manual lifecycle control with `activity start`, `activity update`, and `activity end` | ||
| - `stats`: best for compact business or product stats like revenue, orders, conversion, and average order value | ||
| - `metrics`: best for live operational stats like server CPU and memory, queue depth, or replica lag | ||
| - `segmented_progress`: best for step-based workflows like deployments, backups, and ETL pipelines | ||
| - `progress`: best for continuous jobs like uploads, reindexes, and long-running migrations tracked as a percentage | ||
| Use stream updates when you want the easiest, stateless flow. You do not need to | ||
| store `activity_id` or manage lifecycle state yourself. Send the latest state | ||
| for a stable `stream_key` and ActivitySmith will start or update the Live | ||
| Activity for you. When the tracked process is over, call `activity end-stream`. | ||
| When working with Live Activities via our API, you have two approaches tailored | ||
| to different needs. First, the stateless mode is the simplest path - one API | ||
| call can initiate or update an activity, and another ends it - no state | ||
| tracking on your side. | ||
| Use the manual lifecycle commands when you need direct control over a specific | ||
| Live Activity instance. | ||
| This is ideal if you want minimal complexity, perfect for automated workflows | ||
| like cron jobs. | ||
| Live Activity UI types: | ||
| In contrast, if you need precise lifecycle control, the classic approach offers | ||
| distinct calls for start, updates, and end, giving you full control over the | ||
| activity's state. | ||
| - `metrics`: best for live operational stats like server CPU and memory, queue depth, or replica lag | ||
| - `segmented_progress`: best for step-based workflows like deployments, backups, and ETL pipelines | ||
| - `progress`: best for continuous jobs like uploads, reindexes, and long-running migrations tracked as a percentage | ||
| In the following sections, we'll break down how to implement each method so you | ||
| can choose what fits your use case best. | ||
| ### Recommended: Stream updates | ||
| ### Simple: Let ActivitySmith manage the Live Activity for you | ||
@@ -151,2 +177,25 @@ Use a stable `stream_key` to identify the system or workflow you are tracking, | ||
| #### Stats | ||
| <p align="center"> | ||
| <img src="https://cdn.activitysmith.com/features/stats-live-activity.png" alt="Stats stream example" width="680" /> | ||
| </p> | ||
| ```bash | ||
| activitysmith activity stream sales-hourly \ | ||
| --content-state '{ | ||
| "title": "Sales", | ||
| "subtitle": "last hour", | ||
| "type": "stats", | ||
| "metrics": [ | ||
| { "label": "Revenue", "value": "$2430", "color": "blue" }, | ||
| { "label": "Orders", "value": "37", "color": "green" }, | ||
| { "label": "Conversion", "value": "4.8%", "color": "magenta" }, | ||
| { "label": "Avg Order", "value": "$65.68", "color": "yellow" }, | ||
| { "label": "Refunds", "value": "$84", "color": "red" }, | ||
| { "label": "New Buyers", "value": "18", "color": "cyan" } | ||
| ] | ||
| }' | ||
| ``` | ||
| #### Metrics | ||
@@ -238,8 +287,6 @@ | ||
| ### Advanced: Manual lifecycle control | ||
| ### Advanced: Full lifecycle control | ||
| Use these commands when you want to manage the Live Activity lifecycle yourself. | ||
| Use these commands when you want to manage the Live Activity lifecycle yourself: | ||
| #### Shared flow | ||
| 1. Run `activitysmith activity start ...`. | ||
@@ -253,2 +300,74 @@ 2. Save the returned `activity_id`. | ||
| ### Stats | ||
| Keep your key numbers on your Lock Screen. `stats` fits up to 8 labeled values, | ||
| such as revenue, orders, conversion, uptime, or any other business metric you | ||
| want visible at a glance. Each metric can use a formatted string or number as | ||
| its `value`. Add `color` to a metric to show an accent dot next to its label; | ||
| omit `color` to show the label without a dot. | ||
| #### Start | ||
| <p align="center"> | ||
| <img src="https://cdn.activitysmith.com/features/stats-live-activity.png" alt="Stats Live Activity with sales revenue, orders, conversion, and average order value" width="680" /> | ||
| </p> | ||
| ```bash | ||
| activitysmith activity start \ | ||
| --content-state '{ | ||
| "title": "Sales", | ||
| "subtitle": "last hour", | ||
| "type": "stats", | ||
| "metrics": [ | ||
| { "label": "Revenue", "value": "$2430", "color": "blue" }, | ||
| { "label": "Orders", "value": "37", "color": "green" }, | ||
| { "label": "Conversion", "value": "4.8%", "color": "magenta" }, | ||
| { "label": "Avg Order", "value": "$65.68", "color": "yellow" }, | ||
| { "label": "Refunds", "value": "$84", "color": "red" }, | ||
| { "label": "New Buyers", "value": "18", "color": "cyan" } | ||
| ] | ||
| }' | ||
| ``` | ||
| #### Update | ||
| ```bash | ||
| activitysmith activity update \ | ||
| --activity-id "<activityId>" \ | ||
| --content-state '{ | ||
| "title": "Sales", | ||
| "subtitle": "last hour", | ||
| "type": "stats", | ||
| "metrics": [ | ||
| { "label": "Revenue", "value": "$3180", "color": "blue" }, | ||
| { "label": "Orders", "value": "51", "color": "green" }, | ||
| { "label": "Conversion", "value": "5.2%", "color": "magenta" }, | ||
| { "label": "Avg Order", "value": "$62.35", "color": "yellow" }, | ||
| { "label": "Refunds", "value": "$126", "color": "red" }, | ||
| { "label": "New Buyers", "value": "24", "color": "cyan" } | ||
| ] | ||
| }' | ||
| ``` | ||
| #### End | ||
| ```bash | ||
| activitysmith activity end \ | ||
| --activity-id "<activityId>" \ | ||
| --content-state '{ | ||
| "title": "Sales", | ||
| "subtitle": "last hour", | ||
| "type": "stats", | ||
| "metrics": [ | ||
| { "label": "Revenue", "value": "$3460", "color": "blue" }, | ||
| { "label": "Orders", "value": "58", "color": "green" }, | ||
| { "label": "Conversion", "value": "5.4%", "color": "magenta" }, | ||
| { "label": "Avg Order", "value": "$59.66", "color": "yellow" }, | ||
| { "label": "Refunds", "value": "$92", "color": "red" }, | ||
| { "label": "New Buyers", "value": "31", "color": "cyan" } | ||
| ], | ||
| "autoDismissMinutes": 2 | ||
| }' | ||
| ``` | ||
| ### Metrics Type | ||
@@ -321,6 +440,6 @@ | ||
| Use `segmented_progress` when progress is easier to follow as steps instead of a | ||
| raw percentage. It fits jobs like deployments, backups, ETL pipelines, and | ||
| checklists where "step 2 of 3" is more useful than "67%". `numberOfSteps` is | ||
| dynamic, so you can increase or decrease it later if the workflow changes. | ||
| Use `segmented_progress` for jobs and workflows that move through clear steps or | ||
| phases. It fits jobs like deployments, backups, ETL pipelines, and checklists. | ||
| `numberOfSteps` is dynamic, so you can increase or decrease it later if the | ||
| workflow changes. | ||
@@ -437,3 +556,3 @@ #### Start | ||
| Just like Actionable Push Notifications, Live Activities can have a button that opens a URL in a browser or triggers a webhook. Webhooks are executed by the ActivitySmith backend. | ||
| Just like Actionable Push Notifications, Live Activities can have a button that opens provided URL in a browser or triggers a webhook. Webhooks are executed by the ActivitySmith backend. | ||
@@ -466,2 +585,6 @@ <p align="center"> | ||
| <p align="center"> | ||
| <img src="https://cdn.activitysmith.com/features/live-activity-with-action.png?v=20260319-1" alt="Live Activity with action" width="680" /> | ||
| </p> | ||
| ```bash | ||
@@ -471,17 +594,14 @@ activitysmith activity update \ | ||
| --content-state '{ | ||
| "title": "Server Health", | ||
| "subtitle": "prod-web-1", | ||
| "type": "metrics", | ||
| "metrics": [ | ||
| { "label": "CPU", "value": 91, "unit": "%" }, | ||
| { "label": "MEM", "value": 57, "unit": "%" } | ||
| ] | ||
| "title": "Reindexing product search", | ||
| "subtitle": "Shard 7 of 12", | ||
| "numberOfSteps": 12, | ||
| "currentStep": 7 | ||
| }' \ | ||
| --action '{ | ||
| "title": "Restart Service", | ||
| "title": "Pause Reindex", | ||
| "type": "webhook", | ||
| "url": "https://ops.example.com/hooks/servers/prod-web-1/restart", | ||
| "url": "https://ops.example.com/hooks/search/reindex/pause", | ||
| "method": "POST", | ||
| "body": { | ||
| "server_id": "prod-web-1", | ||
| "job_id": "reindex-2026-03-19", | ||
| "requested_by": "activitysmith-cli" | ||
@@ -503,2 +623,26 @@ } | ||
| ## Widgets | ||
| <p align="center"> | ||
| <img src="https://cdn.activitysmith.com/features/lock-screen-widgets.png" alt="Lock screen widgets" width="680" /> | ||
| </p> | ||
| ActivitySmith lets you display any value on your Lock Screen with widgets - SaaS metrics, revenue, signups, uptime, habits, or anything else you want to track. Create a metric in the <a href="https://activitysmith.com/app/widgets" target="_blank" rel="noopener noreferrer">web app</a>, then update the metric value using our API, add a widget to your lock screen and it will fetch the latest update automatically. | ||
| <p align="center"> | ||
| <img src="https://cdn.activitysmith.com/features/create-widget-metric.png" alt="Create widget metric" width="680" /> | ||
| </p> | ||
| Use the metric key to update its value. | ||
| ```bash | ||
| activitysmith metrics update deploy.success_rate 99.9 | ||
| ``` | ||
| String metric values work too. | ||
| ```bash | ||
| activitysmith metrics update prod.status healthy | ||
| ``` | ||
| ## Aliases | ||
@@ -518,3 +662,3 @@ | ||
| For `metrics`, you can also pass the metrics array directly: | ||
| For `metrics` and `stats`, you can also pass the metrics array directly: | ||
@@ -547,2 +691,7 @@ - `--metrics <json-array>` | ||
| Widget metric options: | ||
| - `activitysmith metrics update <metric-key> <value>` | ||
| - `activitysmith metric update <metric-key> <value>` (alias) | ||
| Required fields: | ||
@@ -549,0 +698,0 @@ |
+94
-8
@@ -57,2 +57,26 @@ #!/usr/bin/env node | ||
| const parseMetricValueArgument = (value) => { | ||
| if (typeof value !== "string") { | ||
| throw new InvalidArgumentError("value must be a string or number"); | ||
| } | ||
| const trimmed = value.trim(); | ||
| if (trimmed.length === 0) { | ||
| throw new InvalidArgumentError("value cannot be empty"); | ||
| } | ||
| try { | ||
| const parsed = JSON.parse(trimmed); | ||
| if (typeof parsed === "string" || typeof parsed === "number") { | ||
| return parsed; | ||
| } | ||
| throw new InvalidArgumentError("value must be a string or number"); | ||
| } catch (error) { | ||
| if (error instanceof InvalidArgumentError) { | ||
| throw error; | ||
| } | ||
| return value; | ||
| } | ||
| }; | ||
| const normalizeHttpsUrl = (value, label) => { | ||
@@ -216,2 +240,14 @@ if (typeof value !== "string" || value.trim().length === 0) { | ||
| const liveActivityMetricColors = new Set([ | ||
| "blue", | ||
| "cyan", | ||
| "green", | ||
| "lime", | ||
| "magenta", | ||
| "orange", | ||
| "purple", | ||
| "red", | ||
| "yellow", | ||
| ]); | ||
| const validateContentState = (contentState, mode) => { | ||
@@ -229,6 +265,7 @@ const normalizedType = | ||
| normalizedType !== "progress" && | ||
| normalizedType !== "metrics" | ||
| normalizedType !== "metrics" && | ||
| normalizedType !== "stats" | ||
| ) { | ||
| throw new Error( | ||
| "contentState.type must be one of: segmented_progress, progress, metrics" | ||
| "contentState.type must be one of: segmented_progress, progress, metrics, stats" | ||
| ); | ||
@@ -290,2 +327,6 @@ } | ||
| if (normalizedType === "stats" && contentState.metrics.length > 8) { | ||
| throw new Error("stats contentState.metrics supports up to 8 items"); | ||
| } | ||
| contentState.metrics.forEach((metric, index) => { | ||
@@ -296,4 +337,12 @@ assertPlainObject(metric, `contentState.metrics[${index}]`); | ||
| } | ||
| if (!Number.isFinite(metric.value)) { | ||
| throw new Error(`contentState.metrics[${index}].value must be a number`); | ||
| if (normalizedType === "stats") { | ||
| if (!Number.isFinite(metric.value) && !isNonEmptyString(metric.value)) { | ||
| throw new Error( | ||
| `contentState.metrics[${index}].value must be a number or non-empty string` | ||
| ); | ||
| } | ||
| } else if (!Number.isFinite(metric.value)) { | ||
| throw new Error( | ||
| `contentState.metrics[${index}].value must be a number. Use contentState.type=stats for string values` | ||
| ); | ||
| } | ||
@@ -303,2 +352,14 @@ if (metric.unit !== undefined && typeof metric.unit !== "string") { | ||
| } | ||
| if (metric.color !== undefined) { | ||
| if (!isNonEmptyString(metric.color)) { | ||
| throw new Error(`contentState.metrics[${index}].color must be a string`); | ||
| } | ||
| if (!liveActivityMetricColors.has(metric.color.trim())) { | ||
| throw new Error( | ||
| `contentState.metrics[${index}].color must be one of: ${[ | ||
| ...liveActivityMetricColors, | ||
| ].join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
@@ -323,5 +384,5 @@ } | ||
| if (effectiveType === "metrics") { | ||
| if (effectiveType === "metrics" || effectiveType === "stats") { | ||
| if (!hasMetrics) { | ||
| throw new Error(`metrics ${mode} requires contentState.metrics`); | ||
| throw new Error(`${effectiveType} ${mode} requires contentState.metrics`); | ||
| } | ||
@@ -357,5 +418,5 @@ return; | ||
| if (effectiveType === "metrics") { | ||
| if (effectiveType === "metrics" || effectiveType === "stats") { | ||
| if (!hasMetrics) { | ||
| throw new Error(`metrics ${mode} requires contentState.metrics`); | ||
| throw new Error(`${effectiveType} ${mode} requires contentState.metrics`); | ||
| } | ||
@@ -914,2 +975,27 @@ return; | ||
| const metricsCommand = program | ||
| .command("metrics") | ||
| .alias("metric") | ||
| .description("Update widget metric values"); | ||
| metricsCommand | ||
| .command("update") | ||
| .description("Update a widget metric value") | ||
| .argument("<metric-key>", "Metric key") | ||
| .argument("<value>", "Metric value") | ||
| .action(async (metricKey, rawValue) => { | ||
| const globalOptions = program.opts(); | ||
| try { | ||
| const apiKey = requireApiKey(globalOptions); | ||
| const client = createClient(apiKey); | ||
| const value = parseMetricValueArgument(rawValue); | ||
| const response = await client.metrics.update(metricKey, value); | ||
| outputResult(response, globalOptions, ["Metric value updated."]); | ||
| } catch (error) { | ||
| await handleError(error, globalOptions); | ||
| } | ||
| }); | ||
| const activityCommand = program | ||
@@ -916,0 +1002,0 @@ .command("activity") |
54033
17.11%984
8.49%700
27.04%Updated