Merge branch 'fix/telegram-empty-response-notification'
This commit is contained in:
commit
60453c912e
@ -249,6 +249,9 @@
|
|||||||
"@sinclair/typebox": "0.34.47",
|
"@sinclair/typebox": "0.34.47",
|
||||||
"hono": "4.11.4",
|
"hono": "4.11.4",
|
||||||
"tar": "7.5.4"
|
"tar": "7.5.4"
|
||||||
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@mariozechner/pi-ai@0.49.3": "patches/@mariozechner__pi-ai@0.49.3.patch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vitest": {
|
"vitest": {
|
||||||
|
|||||||
58
patches/@mariozechner__pi-ai@0.49.3.patch
Normal file
58
patches/@mariozechner__pi-ai@0.49.3.patch
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
diff --git a/dist/providers/openai-completions.js b/dist/providers/openai-completions.js
|
||||||
|
index c4b291d86ce40b2df319c439263d16d67592d22e..449da02d0d2c85309979e392909df5d3fb2ad083 100644
|
||||||
|
--- a/dist/providers/openai-completions.js
|
||||||
|
+++ b/dist/providers/openai-completions.js
|
||||||
|
@@ -228,7 +228,13 @@ export const streamOpenAICompletions = (model, context, options) => {
|
||||||
|
}
|
||||||
|
const reasoningDetails = choice.delta.reasoning_details;
|
||||||
|
if (reasoningDetails && Array.isArray(reasoningDetails)) {
|
||||||
|
+ // Store ALL reasoning_details for OpenRouter compatibility (not just encrypted)
|
||||||
|
+ if (!output._rawReasoningDetails) {
|
||||||
|
+ output._rawReasoningDetails = [];
|
||||||
|
+ }
|
||||||
|
for (const detail of reasoningDetails) {
|
||||||
|
+ output._rawReasoningDetails.push(detail);
|
||||||
|
+ // Also handle encrypted type tied to tool calls (existing behavior)
|
||||||
|
if (detail.type === "reasoning.encrypted" && detail.id && detail.data) {
|
||||||
|
const matchingToolCall = output.content.find((b) => b.type === "toolCall" && b.id === detail.id);
|
||||||
|
if (matchingToolCall) {
|
||||||
|
@@ -500,19 +506,27 @@ function convertMessages(model, context, compat) {
|
||||||
|
arguments: JSON.stringify(tc.arguments),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
- const reasoningDetails = toolCalls
|
||||||
|
- .filter((tc) => tc.thoughtSignature)
|
||||||
|
- .map((tc) => {
|
||||||
|
- try {
|
||||||
|
- return JSON.parse(tc.thoughtSignature);
|
||||||
|
- }
|
||||||
|
- catch {
|
||||||
|
- return null;
|
||||||
|
+ // First check for raw reasoning_details stored on the message (OpenRouter models like Kimi)
|
||||||
|
+ const rawDetails = msg._rawReasoningDetails;
|
||||||
|
+ if (rawDetails && Array.isArray(rawDetails) && rawDetails.length > 0) {
|
||||||
|
+ assistantMsg.reasoning_details = rawDetails;
|
||||||
|
+ }
|
||||||
|
+ else {
|
||||||
|
+ // Fall back to extracting from tool call thoughtSignatures (encrypted type)
|
||||||
|
+ const reasoningDetails = toolCalls
|
||||||
|
+ .filter((tc) => tc.thoughtSignature)
|
||||||
|
+ .map((tc) => {
|
||||||
|
+ try {
|
||||||
|
+ return JSON.parse(tc.thoughtSignature);
|
||||||
|
+ }
|
||||||
|
+ catch {
|
||||||
|
+ return null;
|
||||||
|
+ }
|
||||||
|
+ })
|
||||||
|
+ .filter(Boolean);
|
||||||
|
+ if (reasoningDetails.length > 0) {
|
||||||
|
+ assistantMsg.reasoning_details = reasoningDetails;
|
||||||
|
}
|
||||||
|
- })
|
||||||
|
- .filter(Boolean);
|
||||||
|
- if (reasoningDetails.length > 0) {
|
||||||
|
- assistantMsg.reasoning_details = reasoningDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip assistant messages that have no content and no tool calls.
|
||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -9,6 +9,11 @@ overrides:
|
|||||||
hono: 4.11.4
|
hono: 4.11.4
|
||||||
tar: 7.5.4
|
tar: 7.5.4
|
||||||
|
|
||||||
|
patchedDependencies:
|
||||||
|
'@mariozechner/pi-ai@0.49.3':
|
||||||
|
hash: 8fada8af40beb6c2780b3adec72505288272a5df0ec6227b710caef6afb1dc32
|
||||||
|
path: patches/@mariozechner__pi-ai@0.49.3.patch
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@ -45,7 +50,7 @@ importers:
|
|||||||
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
|
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: 0.49.3
|
specifier: 0.49.3
|
||||||
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
|
version: 0.49.3(patch_hash=8fada8af40beb6c2780b3adec72505288272a5df0ec6227b710caef6afb1dc32)(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: 0.49.3
|
specifier: 0.49.3
|
||||||
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
|
version: 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
@ -6995,7 +7000,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.49.3(ws@8.19.0)(zod@4.3.6)':
|
'@mariozechner/pi-agent-core@0.49.3(ws@8.19.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-ai': 0.49.3(patch_hash=8fada8af40beb6c2780b3adec72505288272a5df0ec6227b710caef6afb1dc32)(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-tui': 0.49.3
|
'@mariozechner/pi-tui': 0.49.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@ -7006,7 +7011,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.49.3(ws@8.19.0)(zod@4.3.6)':
|
'@mariozechner/pi-ai@0.49.3(patch_hash=8fada8af40beb6c2780b3adec72505288272a5df0ec6227b710caef6afb1dc32)(ws@8.19.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.6)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.3.6)
|
||||||
'@aws-sdk/client-bedrock-runtime': 3.972.0
|
'@aws-sdk/client-bedrock-runtime': 3.972.0
|
||||||
@ -7033,7 +7038,7 @@ snapshots:
|
|||||||
'@mariozechner/clipboard': 0.3.0
|
'@mariozechner/clipboard': 0.3.0
|
||||||
'@mariozechner/jiti': 2.6.5
|
'@mariozechner/jiti': 2.6.5
|
||||||
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-ai': 0.49.3(patch_hash=8fada8af40beb6c2780b3adec72505288272a5df0ec6227b710caef6afb1dc32)(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-tui': 0.49.3
|
'@mariozechner/pi-tui': 0.49.3
|
||||||
'@silvia-odwyer/photon-node': 0.3.4
|
'@silvia-odwyer/photon-node': 0.3.4
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
@ -9110,7 +9115,7 @@ snapshots:
|
|||||||
'@line/bot-sdk': 10.6.0
|
'@line/bot-sdk': 10.6.0
|
||||||
'@lydell/node-pty': 1.2.0-beta.3
|
'@lydell/node-pty': 1.2.0-beta.3
|
||||||
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-ai': 0.49.3(patch_hash=8fada8af40beb6c2780b3adec72505288272a5df0ec6227b710caef6afb1dc32)(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
'@mariozechner/pi-tui': 0.49.3
|
'@mariozechner/pi-tui': 0.49.3
|
||||||
'@mozilla/readability': 0.6.0
|
'@mozilla/readability': 0.6.0
|
||||||
|
|||||||
136
skills/ga4/SKILL.md
Normal file
136
skills/ga4/SKILL.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
name: ga4
|
||||||
|
description: GA4 Analytics CLI for users, events, funnels, and transcription metrics.
|
||||||
|
homepage: https://analytics.google.com
|
||||||
|
metadata: {"clawdbot":{"emoji":"📊","requires":{"bins":["bun"],"env":["GOOGLE_APPLICATION_CREDENTIALS"]},"primaryEnv":"GOOGLE_APPLICATION_CREDENTIALS"}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# GA4 Analytics CLI
|
||||||
|
|
||||||
|
Query Google Analytics 4 data - users, events, funnels, transcription metrics.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Set environment variables:
|
||||||
|
```bash
|
||||||
|
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
|
||||||
|
export GA4_PROPERTY_ID=510572418 # Optional, defaults to Speakmac
|
||||||
|
```
|
||||||
|
|
||||||
|
Or configure in `~/.clawdbot/clawdbot.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"skills": {
|
||||||
|
"entries": {
|
||||||
|
"ga4": {
|
||||||
|
"env": {
|
||||||
|
"GOOGLE_APPLICATION_CREDENTIALS": "/path/to/service-account.json",
|
||||||
|
"GA4_PROPERTY_ID": "510572418"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts overview # 30-day overview
|
||||||
|
bun {baseDir}/scripts/cli.ts overview --period 7d # Last 7 days
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-time
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts realtime # Active users now
|
||||||
|
```
|
||||||
|
|
||||||
|
### Events
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts events # Top 50 events
|
||||||
|
bun {baseDir}/scripts/cli.ts events --limit 20 # Limit results
|
||||||
|
bun {baseDir}/scripts/cli.ts events --period 7d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Funnels
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts funnel onboarding # Onboarding completion
|
||||||
|
bun {baseDir}/scripts/cli.ts funnel transcription # Transcription success
|
||||||
|
bun {baseDir}/scripts/cli.ts funnel permissions # Permission grants
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts errors # All errors (7d)
|
||||||
|
bun {baseDir}/scripts/cli.ts errors --type transcription
|
||||||
|
bun {baseDir}/scripts/cli.ts errors --type license
|
||||||
|
bun {baseDir}/scripts/cli.ts errors --period 14d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transcription Metrics
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts transcriptions # By provider
|
||||||
|
bun {baseDir}/scripts/cli.ts transcriptions --by day # Daily breakdown
|
||||||
|
bun {baseDir}/scripts/cli.ts transcriptions --period 14d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Adoption
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts versions # App version usage
|
||||||
|
bun {baseDir}/scripts/cli.ts versions --period 7d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Growth Metrics
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts growth # Growth trends
|
||||||
|
bun {baseDir}/scripts/cli.ts growth --period 90d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Weekly (Date Range)
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts weekly --from 2025-11-01 --to 2026-01-20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Query
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts query --dimensions week --metrics activeUsers,sessions --from 2025-11-01 --to 2026-01-20
|
||||||
|
bun {baseDir}/scripts/cli.ts query --dimensions date --metrics eventCount --period 30d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Periods
|
||||||
|
|
||||||
|
- `today` - Current day
|
||||||
|
- `yesterday` - Previous day
|
||||||
|
- `7d` - Last 7 days
|
||||||
|
- `14d` - Last 14 days
|
||||||
|
- `28d` - Last 28 days
|
||||||
|
- `30d` - Last 30 days (default)
|
||||||
|
- `90d` - Last 90 days
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Daily check-in
|
||||||
|
bun {baseDir}/scripts/cli.ts overview --period 7d
|
||||||
|
bun {baseDir}/scripts/cli.ts realtime
|
||||||
|
|
||||||
|
# Feature usage
|
||||||
|
bun {baseDir}/scripts/cli.ts events --limit 20
|
||||||
|
|
||||||
|
# Onboarding health
|
||||||
|
bun {baseDir}/scripts/cli.ts funnel onboarding
|
||||||
|
|
||||||
|
# Transcription performance
|
||||||
|
bun {baseDir}/scripts/cli.ts transcriptions --by provider
|
||||||
|
bun {baseDir}/scripts/cli.ts errors --type transcription
|
||||||
|
|
||||||
|
# Version rollout
|
||||||
|
bun {baseDir}/scripts/cli.ts versions --period 7d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Service account needs "Viewer" role on GA4 property
|
||||||
|
- Real-time data shows last 30 minutes
|
||||||
|
- See `{baseDir}/docs/API_REFERENCE.md` for full API documentation
|
||||||
671
skills/ga4/scripts/cli.ts
Normal file
671
skills/ga4/scripts/cli.ts
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* GA4 Analytics CLI - Speakmac analytics at your fingertips
|
||||||
|
* Usage: bun skills/ga4/cli.ts <command> [options]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BetaAnalyticsDataClient } from "@google-analytics/data";
|
||||||
|
|
||||||
|
const CREDENTIALS_PATH = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||||
|
const PROPERTY_ID = process.env.GA4_PROPERTY_ID || "510572418";
|
||||||
|
|
||||||
|
if (!CREDENTIALS_PATH) {
|
||||||
|
console.error("Error: GOOGLE_APPLICATION_CREDENTIALS environment variable required");
|
||||||
|
console.error("Point it to your service account JSON file");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client: BetaAnalyticsDataClient | null = null;
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
if (!client) {
|
||||||
|
client = new BetaAnalyticsDataClient();
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateRange = { startDate: string; endDate: string };
|
||||||
|
|
||||||
|
function getDateRange(period: string): DateRange {
|
||||||
|
const ranges: Record<string, DateRange> = {
|
||||||
|
today: { startDate: "today", endDate: "today" },
|
||||||
|
yesterday: { startDate: "yesterday", endDate: "yesterday" },
|
||||||
|
"7d": { startDate: "7daysAgo", endDate: "today" },
|
||||||
|
"14d": { startDate: "14daysAgo", endDate: "today" },
|
||||||
|
"28d": { startDate: "28daysAgo", endDate: "today" },
|
||||||
|
"30d": { startDate: "30daysAgo", endDate: "today" },
|
||||||
|
"90d": { startDate: "90daysAgo", endDate: "today" },
|
||||||
|
};
|
||||||
|
return ranges[period] || ranges["30d"];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runReport(
|
||||||
|
dimensions: string[],
|
||||||
|
metrics: string[],
|
||||||
|
dateRange: DateRange,
|
||||||
|
filter?: any,
|
||||||
|
limit = 100
|
||||||
|
) {
|
||||||
|
const c = getClient();
|
||||||
|
const request: any = {
|
||||||
|
property: `properties/${PROPERTY_ID}`,
|
||||||
|
dateRanges: [dateRange],
|
||||||
|
dimensions: dimensions.map((d) => ({ name: d })),
|
||||||
|
metrics: metrics.map((m) => ({ name: m })),
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
if (filter) request.dimensionFilter = filter;
|
||||||
|
|
||||||
|
const [response] = await c.runReport(request);
|
||||||
|
|
||||||
|
const results: Record<string, any>[] = [];
|
||||||
|
if (response.rows) {
|
||||||
|
for (const row of response.rows) {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
dimensions.forEach((dim, i) => {
|
||||||
|
result[dim] = row.dimensionValues?.[i]?.value || "(not set)";
|
||||||
|
});
|
||||||
|
metrics.forEach((metric, i) => {
|
||||||
|
result[metric] = row.metricValues?.[i]?.value || "0";
|
||||||
|
});
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowCount: response.rowCount || 0,
|
||||||
|
results,
|
||||||
|
totals:
|
||||||
|
response.totals?.[0]?.metricValues?.map((m, i) => ({
|
||||||
|
metric: metrics[i],
|
||||||
|
value: m.value,
|
||||||
|
})) || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
async function overview(period: string) {
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
const data = await runReport(
|
||||||
|
["date"],
|
||||||
|
[
|
||||||
|
"activeUsers",
|
||||||
|
"newUsers",
|
||||||
|
"sessions",
|
||||||
|
"eventCount",
|
||||||
|
"engagementRate",
|
||||||
|
"averageSessionDuration",
|
||||||
|
],
|
||||||
|
dateRange,
|
||||||
|
null,
|
||||||
|
50
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aggregate totals
|
||||||
|
let users = 0,
|
||||||
|
newU = 0,
|
||||||
|
sessions = 0,
|
||||||
|
events = 0;
|
||||||
|
for (const row of data.results) {
|
||||||
|
users += parseInt(row.activeUsers);
|
||||||
|
newU += parseInt(row.newUsers);
|
||||||
|
sessions += parseInt(row.sessions);
|
||||||
|
events += parseInt(row.eventCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 GA4 Overview (${period})\n`);
|
||||||
|
console.log(`Active Users: ${users}`);
|
||||||
|
console.log(`New Users: ${newU}`);
|
||||||
|
console.log(`Sessions: ${sessions}`);
|
||||||
|
console.log(`Total Events: ${events}`);
|
||||||
|
|
||||||
|
if (data.totals.length > 0) {
|
||||||
|
const engRate = data.totals.find((t) => t.metric === "engagementRate");
|
||||||
|
const avgDur = data.totals.find(
|
||||||
|
(t) => t.metric === "averageSessionDuration"
|
||||||
|
);
|
||||||
|
if (engRate)
|
||||||
|
console.log(
|
||||||
|
`Engagement: ${(parseFloat(engRate.value || "0") * 100).toFixed(1)}%`
|
||||||
|
);
|
||||||
|
if (avgDur)
|
||||||
|
console.log(
|
||||||
|
`Avg Session: ${Math.round(parseFloat(avgDur.value || "0"))}s`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function realtime() {
|
||||||
|
const c = getClient();
|
||||||
|
const [response] = await c.runRealtimeReport({
|
||||||
|
property: `properties/${PROPERTY_ID}`,
|
||||||
|
dimensions: [{ name: "unifiedScreenName" }],
|
||||||
|
metrics: [{ name: "activeUsers" }, { name: "eventCount" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalUsers =
|
||||||
|
response.totals?.[0]?.metricValues?.[0]?.value || "0";
|
||||||
|
|
||||||
|
console.log(`⚡ Real-time (last 30 min)\n`);
|
||||||
|
console.log(`Active Users: ${totalUsers}\n`);
|
||||||
|
|
||||||
|
if (response.rows && response.rows.length > 0) {
|
||||||
|
console.log("By Screen:");
|
||||||
|
for (const row of response.rows.slice(0, 10)) {
|
||||||
|
const screen = row.dimensionValues?.[0]?.value || "(unknown)";
|
||||||
|
const users = row.metricValues?.[0]?.value || "0";
|
||||||
|
console.log(` ${users.padStart(3)} | ${screen}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function events(period: string, limit: number) {
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
const data = await runReport(
|
||||||
|
["eventName"],
|
||||||
|
["eventCount", "eventCountPerUser"],
|
||||||
|
dateRange,
|
||||||
|
null,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`📈 Events (${period})\n`);
|
||||||
|
console.log("Event".padEnd(45) + "Count".padStart(10) + "Per User".padStart(10));
|
||||||
|
console.log("-".repeat(65));
|
||||||
|
|
||||||
|
for (const row of data.results) {
|
||||||
|
const name = row.eventName.substring(0, 44);
|
||||||
|
const count = row.eventCount;
|
||||||
|
const perUser = parseFloat(row.eventCountPerUser).toFixed(2);
|
||||||
|
console.log(
|
||||||
|
name.padEnd(45) + count.toString().padStart(10) + perUser.padStart(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function funnel(funnelType: string, period: string) {
|
||||||
|
const funnels: Record<string, string[]> = {
|
||||||
|
onboarding: [
|
||||||
|
"onboarding_started",
|
||||||
|
"onboarding_step_completed",
|
||||||
|
"permission_prompt_shown",
|
||||||
|
"permission_result",
|
||||||
|
"demo_transcription_started",
|
||||||
|
"demo_transcription_completed",
|
||||||
|
"onboarding_completed",
|
||||||
|
],
|
||||||
|
transcription: [
|
||||||
|
"transcription_started",
|
||||||
|
"transcription_completed",
|
||||||
|
"transcription_failed",
|
||||||
|
],
|
||||||
|
permissions: ["permission_prompt_shown", "permission_result"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = funnels[funnelType];
|
||||||
|
if (!events) {
|
||||||
|
console.error(`Unknown funnel: ${funnelType}`);
|
||||||
|
console.error(`Available: ${Object.keys(funnels).join(", ")}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
console.log(`🔄 ${funnelType} Funnel (${period})\n`);
|
||||||
|
|
||||||
|
let firstCount = 0;
|
||||||
|
for (const event of events) {
|
||||||
|
const filter = {
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: { value: event, matchType: "EXACT" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const data = await runReport(
|
||||||
|
["eventName"],
|
||||||
|
["eventCount", "totalUsers"],
|
||||||
|
dateRange,
|
||||||
|
filter,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const count = parseInt(data.results[0]?.eventCount || "0");
|
||||||
|
const users = parseInt(data.results[0]?.totalUsers || "0");
|
||||||
|
|
||||||
|
if (firstCount === 0) firstCount = count;
|
||||||
|
const pct = firstCount > 0 ? ((count / firstCount) * 100).toFixed(0) : "0";
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${event.padEnd(35)} ${count.toString().padStart(6)} events (${pct}%)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function errors(errorType: string, period: string) {
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
|
||||||
|
let filter: any;
|
||||||
|
if (errorType === "transcription") {
|
||||||
|
filter = {
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: { value: "transcription_failed", matchType: "EXACT" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (errorType === "license") {
|
||||||
|
filter = {
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: {
|
||||||
|
value: "license_validation_error",
|
||||||
|
matchType: "EXACT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
filter = {
|
||||||
|
orGroup: {
|
||||||
|
expressions: [
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: { value: "transcription_failed", matchType: "EXACT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: {
|
||||||
|
value: "license_validation_error",
|
||||||
|
matchType: "EXACT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await runReport(
|
||||||
|
["eventName"],
|
||||||
|
["eventCount"],
|
||||||
|
dateRange,
|
||||||
|
filter,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`❌ Errors (${period})\n`);
|
||||||
|
|
||||||
|
if (data.results.length === 0) {
|
||||||
|
console.log("No errors found! 🎉");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of data.results) {
|
||||||
|
console.log(`${row.eventName}: ${row.eventCount}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transcriptions(period: string, breakdownBy: string) {
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
const filter = {
|
||||||
|
orGroup: {
|
||||||
|
expressions: [
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: { value: "transcription_started", matchType: "EXACT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: { value: "transcription_completed", matchType: "EXACT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
fieldName: "eventName",
|
||||||
|
stringFilter: { value: "transcription_failed", matchType: "EXACT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`🎤 Transcriptions (${period})\n`);
|
||||||
|
|
||||||
|
if (breakdownBy === "day") {
|
||||||
|
// Group by date
|
||||||
|
const data = await runReport(
|
||||||
|
["date", "eventName"],
|
||||||
|
["eventCount"],
|
||||||
|
dateRange,
|
||||||
|
filter,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
const byDate: Record<string, Record<string, number>> = {};
|
||||||
|
for (const row of data.results) {
|
||||||
|
const date = row.date;
|
||||||
|
const event = row.eventName.replace("transcription_", "");
|
||||||
|
if (!byDate[date]) byDate[date] = {};
|
||||||
|
byDate[date][event] = parseInt(row.eventCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Date".padEnd(12) + "Started".padStart(10) + "Completed".padStart(12) + "Failed".padStart(10) + "Success".padStart(10));
|
||||||
|
console.log("-".repeat(54));
|
||||||
|
|
||||||
|
const dates = Object.entries(byDate).sort(([a], [b]) => b.localeCompare(a));
|
||||||
|
for (const [date, events] of dates) {
|
||||||
|
const started = events.started || 0;
|
||||||
|
const completed = events.completed || 0;
|
||||||
|
const failed = events.failed || 0;
|
||||||
|
const successRate = started > 0 ? `${((completed / started) * 100).toFixed(0)}%` : "N/A";
|
||||||
|
console.log(
|
||||||
|
date.padEnd(12) +
|
||||||
|
started.toString().padStart(10) +
|
||||||
|
completed.toString().padStart(12) +
|
||||||
|
failed.toString().padStart(10) +
|
||||||
|
successRate.padStart(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Summary totals
|
||||||
|
const data = await runReport(
|
||||||
|
["eventName"],
|
||||||
|
["eventCount", "totalUsers"],
|
||||||
|
dateRange,
|
||||||
|
filter,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
let started = 0, completed = 0, failed = 0;
|
||||||
|
for (const row of data.results) {
|
||||||
|
if (row.eventName === "transcription_started") started = parseInt(row.eventCount);
|
||||||
|
if (row.eventName === "transcription_completed") completed = parseInt(row.eventCount);
|
||||||
|
if (row.eventName === "transcription_failed") failed = parseInt(row.eventCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const successRate = started > 0 ? ((completed / started) * 100).toFixed(1) : "N/A";
|
||||||
|
console.log(`Started: ${started}`);
|
||||||
|
console.log(`Completed: ${completed}`);
|
||||||
|
console.log(`Failed: ${failed}`);
|
||||||
|
console.log(`Success Rate: ${successRate}%`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function versions(period: string) {
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
const data = await runReport(
|
||||||
|
["appVersion"],
|
||||||
|
["activeUsers", "newUsers", "sessions"],
|
||||||
|
dateRange,
|
||||||
|
null,
|
||||||
|
20
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`📱 Version Adoption (${period})\n`);
|
||||||
|
console.log(
|
||||||
|
"Version".padEnd(20) +
|
||||||
|
"Users".padStart(8) +
|
||||||
|
"New".padStart(8) +
|
||||||
|
"Sessions".padStart(10)
|
||||||
|
);
|
||||||
|
console.log("-".repeat(46));
|
||||||
|
|
||||||
|
for (const row of data.results) {
|
||||||
|
const ver = row.appVersion || "(unknown)";
|
||||||
|
console.log(
|
||||||
|
ver.padEnd(20) +
|
||||||
|
row.activeUsers.padStart(8) +
|
||||||
|
row.newUsers.padStart(8) +
|
||||||
|
row.sessions.padStart(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function growth(period: string) {
|
||||||
|
const dateRange = getDateRange(period);
|
||||||
|
const data = await runReport(
|
||||||
|
["date"],
|
||||||
|
["activeUsers", "newUsers", "totalUsers", "sessions"],
|
||||||
|
dateRange,
|
||||||
|
null,
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get latest vs earliest for comparison
|
||||||
|
const sorted = data.results.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
|
let totalNew = 0,
|
||||||
|
totalActive = 0;
|
||||||
|
for (const row of sorted) {
|
||||||
|
totalNew += parseInt(row.newUsers);
|
||||||
|
totalActive += parseInt(row.activeUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📈 Growth Metrics (${period})\n`);
|
||||||
|
console.log(`Total New Users: ${totalNew}`);
|
||||||
|
console.log(`Total Active Users: ${totalActive}`);
|
||||||
|
console.log(`Days Tracked: ${sorted.length}`);
|
||||||
|
|
||||||
|
if (sorted.length >= 2) {
|
||||||
|
const first = sorted[0];
|
||||||
|
const last = sorted[sorted.length - 1];
|
||||||
|
console.log(`\nFirst Day (${first.date}): ${first.activeUsers} active`);
|
||||||
|
console.log(`Last Day (${last.date}): ${last.activeUsers} active`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function query(
|
||||||
|
dimensions: string[],
|
||||||
|
metrics: string[],
|
||||||
|
period: string,
|
||||||
|
from?: string,
|
||||||
|
to?: string
|
||||||
|
) {
|
||||||
|
let dateRange: DateRange;
|
||||||
|
if (from && to) {
|
||||||
|
dateRange = { startDate: from, endDate: to };
|
||||||
|
} else {
|
||||||
|
dateRange = getDateRange(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await runReport(dimensions, metrics, dateRange, null, 500);
|
||||||
|
|
||||||
|
console.log(`📊 Custom Query (${from || period}${to ? ` to ${to}` : ""})\n`);
|
||||||
|
console.log(`Rows: ${data.rowCount}\n`);
|
||||||
|
|
||||||
|
// Print header
|
||||||
|
const header = [...dimensions, ...metrics].map((h) => h.padEnd(15)).join(" | ");
|
||||||
|
console.log(header);
|
||||||
|
console.log("-".repeat(header.length));
|
||||||
|
|
||||||
|
// Print rows
|
||||||
|
for (const row of data.results) {
|
||||||
|
const values = [...dimensions, ...metrics]
|
||||||
|
.map((key) => String(row[key] || "").padEnd(15))
|
||||||
|
.join(" | ");
|
||||||
|
console.log(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print totals if available
|
||||||
|
if (data.totals.length > 0) {
|
||||||
|
console.log("\nTotals:");
|
||||||
|
for (const t of data.totals) {
|
||||||
|
console.log(` ${t.metric}: ${t.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function weekly(from: string, to: string) {
|
||||||
|
const dateRange = { startDate: from, endDate: to };
|
||||||
|
const data = await runReport(
|
||||||
|
["week", "date"],
|
||||||
|
["activeUsers", "newUsers", "sessions"],
|
||||||
|
dateRange,
|
||||||
|
null,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group by week
|
||||||
|
const byWeek: Record<string, { users: number; newUsers: number; sessions: number; dates: string[] }> = {};
|
||||||
|
|
||||||
|
for (const row of data.results) {
|
||||||
|
const week = row.week;
|
||||||
|
if (!byWeek[week]) {
|
||||||
|
byWeek[week] = { users: 0, newUsers: 0, sessions: 0, dates: [] };
|
||||||
|
}
|
||||||
|
byWeek[week].users += parseInt(row.activeUsers);
|
||||||
|
byWeek[week].newUsers += parseInt(row.newUsers);
|
||||||
|
byWeek[week].sessions += parseInt(row.sessions);
|
||||||
|
byWeek[week].dates.push(row.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 Weekly Traffic (${from} to ${to})\n`);
|
||||||
|
console.log("Week".padEnd(6) + "Users".padStart(8) + "New".padStart(8) + "Sessions".padStart(10));
|
||||||
|
console.log("-".repeat(32));
|
||||||
|
|
||||||
|
const weeks = Object.entries(byWeek).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
for (const [week, stats] of weeks) {
|
||||||
|
console.log(
|
||||||
|
week.padEnd(6) +
|
||||||
|
stats.users.toString().padStart(8) +
|
||||||
|
stats.newUsers.toString().padStart(8) +
|
||||||
|
stats.sessions.toString().padStart(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help
|
||||||
|
function help() {
|
||||||
|
console.log(`
|
||||||
|
GA4 Analytics CLI (Speakmac)
|
||||||
|
|
||||||
|
Usage: ga4 <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
overview [--period 30d] Key metrics overview
|
||||||
|
realtime Active users right now
|
||||||
|
events [--period 30d] [--limit N] Event counts
|
||||||
|
funnel <type> [--period 30d] Funnel analysis
|
||||||
|
Types: onboarding, transcription, permissions
|
||||||
|
errors [--type all] [--period 7d] Error analysis
|
||||||
|
Types: all, transcription, license
|
||||||
|
transcriptions [--period 7d] Transcription stats
|
||||||
|
--by day Daily breakdown
|
||||||
|
versions [--period 14d] App version adoption
|
||||||
|
growth [--period 30d] Growth metrics
|
||||||
|
weekly --from YYYY-MM-DD --to YYYY-MM-DD
|
||||||
|
Week-by-week traffic breakdown
|
||||||
|
query --dimensions d1,d2 --metrics m1,m2 [--from DATE --to DATE]
|
||||||
|
Custom query (like MCP ga4_custom_query)
|
||||||
|
|
||||||
|
Periods: today, yesterday, 7d, 14d, 28d, 30d, 90d
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS Path to service account JSON
|
||||||
|
GA4_PROPERTY_ID GA4 property ID (default: 510572418)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ga4 overview
|
||||||
|
ga4 realtime
|
||||||
|
ga4 events --limit 20
|
||||||
|
ga4 funnel onboarding
|
||||||
|
ga4 transcriptions --by day
|
||||||
|
ga4 versions --period 7d
|
||||||
|
ga4 weekly --from 2025-11-01 --to 2026-01-20
|
||||||
|
ga4 query --dimensions week --metrics activeUsers,sessions --from 2025-11-01 --to 2026-01-20
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const cmd = args[0];
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
const flags: Record<string, string> = {};
|
||||||
|
let positional: string[] = [];
|
||||||
|
for (let i = 1; i < args.length; i++) {
|
||||||
|
if (args[i].startsWith("--")) {
|
||||||
|
const key = args[i].slice(2);
|
||||||
|
flags[key] = args[i + 1] || "true";
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
positional.push(args[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const period = flags.period || "30d";
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (cmd) {
|
||||||
|
case "overview":
|
||||||
|
await overview(period);
|
||||||
|
break;
|
||||||
|
case "realtime":
|
||||||
|
await realtime();
|
||||||
|
break;
|
||||||
|
case "events":
|
||||||
|
await events(period, parseInt(flags.limit) || 50);
|
||||||
|
break;
|
||||||
|
case "funnel":
|
||||||
|
const funnelType = positional[0];
|
||||||
|
if (!funnelType) {
|
||||||
|
console.error("Usage: ga4 funnel <type>");
|
||||||
|
console.error("Types: onboarding, transcription, permissions");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await funnel(funnelType, period);
|
||||||
|
break;
|
||||||
|
case "errors":
|
||||||
|
await errors(flags.type || "all", flags.period || "7d");
|
||||||
|
break;
|
||||||
|
case "transcriptions":
|
||||||
|
await transcriptions(flags.period || "7d", flags.by || "summary");
|
||||||
|
break;
|
||||||
|
case "versions":
|
||||||
|
await versions(flags.period || "14d");
|
||||||
|
break;
|
||||||
|
case "growth":
|
||||||
|
await growth(period);
|
||||||
|
break;
|
||||||
|
case "weekly":
|
||||||
|
if (!flags.from || !flags.to) {
|
||||||
|
console.error("Usage: ga4 weekly --from YYYY-MM-DD --to YYYY-MM-DD");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await weekly(flags.from, flags.to);
|
||||||
|
break;
|
||||||
|
case "query":
|
||||||
|
if (!flags.dimensions || !flags.metrics) {
|
||||||
|
console.error("Usage: ga4 query --dimensions d1,d2 --metrics m1,m2 [--from DATE --to DATE]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await query(
|
||||||
|
flags.dimensions.split(","),
|
||||||
|
flags.metrics.split(","),
|
||||||
|
period,
|
||||||
|
flags.from,
|
||||||
|
flags.to
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "help":
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
case undefined:
|
||||||
|
help();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Unknown command: ${cmd}`);
|
||||||
|
help();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
114
skills/lemonsqueezy/SKILL.md
Normal file
114
skills/lemonsqueezy/SKILL.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
name: lemonsqueezy
|
||||||
|
description: LemonSqueezy CLI for revenue, orders, subscriptions, and customer data.
|
||||||
|
homepage: https://lemonsqueezy.com
|
||||||
|
metadata: {"clawdbot":{"emoji":"🍋","requires":{"bins":["bun"],"env":["LEMONSQUEEZY_API_KEY"]},"primaryEnv":"LEMONSQUEEZY_API_KEY"}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# LemonSqueezy CLI
|
||||||
|
|
||||||
|
Query revenue, orders, subscriptions, and customers from LemonSqueezy.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Set the API key:
|
||||||
|
```bash
|
||||||
|
export LEMONSQUEEZY_API_KEY="your_api_key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or configure in `~/.clawdbot/clawdbot.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"skills": {
|
||||||
|
"entries": {
|
||||||
|
"lemonsqueezy": {
|
||||||
|
"apiKey": "your_api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Revenue Overview
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts overview
|
||||||
|
```
|
||||||
|
|
||||||
|
### Orders
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts orders # Recent orders
|
||||||
|
bun {baseDir}/scripts/cli.ts orders --limit 10 # Limit results
|
||||||
|
bun {baseDir}/scripts/cli.ts orders --status paid # Filter: paid/refunded/pending
|
||||||
|
bun {baseDir}/scripts/cli.ts orders --from 2025-11-01 --to 2026-01-20 # Date range with week-by-week summary
|
||||||
|
```
|
||||||
|
|
||||||
|
### Order Lookup
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts order 12345 # By order number
|
||||||
|
bun {baseDir}/scripts/cli.ts order customer@email.com # By customer email
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscriptions
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts subs # All with status breakdown
|
||||||
|
bun {baseDir}/scripts/cli.ts subs --status active # Filter by status
|
||||||
|
```
|
||||||
|
|
||||||
|
Status values: active, cancelled, expired, on_trial, paused, past_due, unpaid
|
||||||
|
|
||||||
|
### MRR Breakdown
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts mrr # MRR by product/variant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customers
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts customers # Customer list with LTV
|
||||||
|
bun {baseDir}/scripts/cli.ts customers --limit 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Products
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts products # All products with URLs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Refunds
|
||||||
|
```bash
|
||||||
|
bun {baseDir}/scripts/cli.ts refunds # Refunded orders
|
||||||
|
bun {baseDir}/scripts/cli.ts refunds --limit 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Icons
|
||||||
|
|
||||||
|
- ✓ = paid
|
||||||
|
- ↩ = refunded
|
||||||
|
- ○ = pending
|
||||||
|
- ● = active subscription
|
||||||
|
- ◐ = other status
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Daily revenue check
|
||||||
|
bun {baseDir}/scripts/cli.ts overview
|
||||||
|
|
||||||
|
# Recent sales
|
||||||
|
bun {baseDir}/scripts/cli.ts orders --limit 5
|
||||||
|
|
||||||
|
# Find customer orders
|
||||||
|
bun {baseDir}/scripts/cli.ts order support@example.com
|
||||||
|
|
||||||
|
# Subscription health
|
||||||
|
bun {baseDir}/scripts/cli.ts subs --status active
|
||||||
|
|
||||||
|
# Track refunds
|
||||||
|
bun {baseDir}/scripts/cli.ts refunds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Revenue displayed in USD
|
||||||
|
- API rate limit: 300 calls/minute
|
||||||
|
- See `{baseDir}/docs/API_REFERENCE.md` for full API documentation
|
||||||
395
skills/lemonsqueezy/scripts/cli.ts
Normal file
395
skills/lemonsqueezy/scripts/cli.ts
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* LemonSqueezy CLI - Revenue, orders, subscriptions at your fingertips
|
||||||
|
* Usage: bun skills/lemonsqueezy/cli.ts <command> [options]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_KEY = process.env.LEMONSQUEEZY_API_KEY;
|
||||||
|
const BASE_URL = "https://api.lemonsqueezy.com/v1";
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error("Error: LEMONSQUEEZY_API_KEY environment variable required");
|
||||||
|
console.error("Set it in your shell or source from SECRETS.env");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(endpoint: string, params: Record<string, string> = {}) {
|
||||||
|
const url = new URL(`${BASE_URL}${endpoint}`);
|
||||||
|
Object.entries(params).forEach(([k, v]) => {
|
||||||
|
if (v) url.searchParams.append(k, v);
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${API_KEY}`,
|
||||||
|
Accept: "application/vnd.api+json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cents(n: number): string {
|
||||||
|
return `$${(n / 100).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string): string {
|
||||||
|
return new Date(iso).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
async function overview() {
|
||||||
|
const { data } = await api("/stores");
|
||||||
|
const store = data[0]?.attributes;
|
||||||
|
if (!store) {
|
||||||
|
console.log("No stores found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 ${store.name} Revenue Overview\n`);
|
||||||
|
console.log(`Total Revenue: ${cents(store.total_revenue)}`);
|
||||||
|
console.log(`Total Sales: ${store.total_sales}`);
|
||||||
|
console.log(`30-Day Revenue: ${cents(store.thirty_day_revenue)}`);
|
||||||
|
console.log(`30-Day Sales: ${store.thirty_day_sales}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function orders(limit = 20, status?: string, from?: string, to?: string) {
|
||||||
|
const params: Record<string, string> = { "page[size]": String(limit) };
|
||||||
|
if (status) params["filter[status]"] = status;
|
||||||
|
|
||||||
|
const { data } = await api("/orders", params);
|
||||||
|
|
||||||
|
// Filter by date if provided
|
||||||
|
let filtered = data;
|
||||||
|
if (from || to) {
|
||||||
|
const fromDate = from ? new Date(from) : new Date(0);
|
||||||
|
const toDate = to ? new Date(to) : new Date();
|
||||||
|
toDate.setHours(23, 59, 59, 999); // End of day
|
||||||
|
filtered = data.filter((order: any) => {
|
||||||
|
const created = new Date(order.attributes.created_at);
|
||||||
|
return created >= fromDate && created <= toDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateRange = from || to ? ` (${from || "start"} to ${to || "now"})` : "";
|
||||||
|
console.log(`📦 Orders${dateRange} (${filtered.length})\n`);
|
||||||
|
|
||||||
|
// Group by week for summary if date range specified
|
||||||
|
if (from && to) {
|
||||||
|
const byWeek: Record<string, { count: number; revenue: number }> = {};
|
||||||
|
for (const order of filtered) {
|
||||||
|
const a = order.attributes;
|
||||||
|
const date = new Date(a.created_at);
|
||||||
|
const week = getWeekNumber(date);
|
||||||
|
const key = `${date.getFullYear()}-W${week.toString().padStart(2, "0")}`;
|
||||||
|
if (!byWeek[key]) byWeek[key] = { count: 0, revenue: 0 };
|
||||||
|
byWeek[key].count++;
|
||||||
|
byWeek[key].revenue += parseInt(a.total_usd || 0) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Week".padEnd(10) + "Orders".padStart(8) + "Revenue".padStart(12));
|
||||||
|
console.log("-".repeat(30));
|
||||||
|
const weeks = Object.entries(byWeek).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
for (const [week, stats] of weeks) {
|
||||||
|
console.log(
|
||||||
|
week.padEnd(10) +
|
||||||
|
stats.count.toString().padStart(8) +
|
||||||
|
`$${stats.revenue.toFixed(2)}`.padStart(12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual orders
|
||||||
|
for (const order of filtered) {
|
||||||
|
const a = order.attributes;
|
||||||
|
const statusIcon = a.status === "paid" ? "✓" : a.status === "refunded" ? "↩" : "○";
|
||||||
|
console.log(
|
||||||
|
`${statusIcon} #${a.order_number} | ${a.total_formatted.padEnd(10)} | ${a.user_email} | ${formatDate(a.created_at)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekNumber(date: Date): number {
|
||||||
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||||
|
const dayNum = d.getUTCDay() || 7;
|
||||||
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||||
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||||
|
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function orderLookup(query: string) {
|
||||||
|
// Try by order number first, then by email
|
||||||
|
let orders: any[] = [];
|
||||||
|
|
||||||
|
if (query.includes("@")) {
|
||||||
|
const { data } = await api("/orders", { "filter[user_email]": query });
|
||||||
|
orders = data;
|
||||||
|
} else {
|
||||||
|
const { data } = await api("/orders");
|
||||||
|
orders = data.filter(
|
||||||
|
(o: any) => o.attributes.order_number.toString() === query
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orders.length === 0) {
|
||||||
|
console.log(`No orders found for: ${query}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 Orders for ${query}\n`);
|
||||||
|
for (const order of orders) {
|
||||||
|
const a = order.attributes;
|
||||||
|
console.log(`Order #${a.order_number}`);
|
||||||
|
console.log(` Status: ${a.status}${a.refunded ? " (refunded)" : ""}`);
|
||||||
|
console.log(` Total: ${a.total_formatted}`);
|
||||||
|
console.log(` Customer: ${a.user_name} <${a.user_email}>`);
|
||||||
|
console.log(` Date: ${formatDate(a.created_at)}`);
|
||||||
|
if (a.refunded_at) console.log(` Refunded: ${formatDate(a.refunded_at)}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscriptions(status?: string) {
|
||||||
|
const params: Record<string, string> = { "page[size]": "100" };
|
||||||
|
if (status) params["filter[status]"] = status;
|
||||||
|
|
||||||
|
const { data } = await api("/subscriptions", params);
|
||||||
|
|
||||||
|
// Calculate MRR
|
||||||
|
let mrr = 0;
|
||||||
|
const byStatus: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const sub of data) {
|
||||||
|
const s = sub.attributes;
|
||||||
|
byStatus[s.status] = (byStatus[s.status] || 0) + 1;
|
||||||
|
if (s.status === "active") {
|
||||||
|
// Assuming monthly - would need price endpoint for accuracy
|
||||||
|
mrr += parseInt(s.first_subscription_item?.price || 0) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`💳 Subscriptions (${data.length})\n`);
|
||||||
|
console.log("By Status:");
|
||||||
|
for (const [st, count] of Object.entries(byStatus)) {
|
||||||
|
console.log(` ${st}: ${count}`);
|
||||||
|
}
|
||||||
|
console.log(`\nEstimated MRR: $${mrr.toFixed(2)}\n`);
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
console.log("Recent:");
|
||||||
|
for (const sub of data.slice(0, 10)) {
|
||||||
|
const s = sub.attributes;
|
||||||
|
const icon =
|
||||||
|
s.status === "active" ? "●" : s.status === "cancelled" ? "○" : "◐";
|
||||||
|
console.log(
|
||||||
|
` ${icon} ${s.user_email.padEnd(30)} | ${s.status.padEnd(10)} | ${s.product_name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mrr() {
|
||||||
|
const { data } = await api("/subscriptions", {
|
||||||
|
"filter[status]": "active",
|
||||||
|
"page[size]": "100",
|
||||||
|
});
|
||||||
|
|
||||||
|
const byProduct: Record<string, { count: number; mrr: number }> = {};
|
||||||
|
|
||||||
|
for (const sub of data) {
|
||||||
|
const s = sub.attributes;
|
||||||
|
const key = `${s.product_name} - ${s.variant_name}`;
|
||||||
|
if (!byProduct[key]) byProduct[key] = { count: 0, mrr: 0 };
|
||||||
|
byProduct[key].count++;
|
||||||
|
// Price from first_subscription_item
|
||||||
|
const price = parseInt(s.first_subscription_item?.price || 0) / 100;
|
||||||
|
byProduct[key].mrr += price;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = Object.entries(byProduct).sort(
|
||||||
|
([, a], [, b]) => b.mrr - a.mrr
|
||||||
|
);
|
||||||
|
const totalMRR = sorted.reduce((sum, [, v]) => sum + v.mrr, 0);
|
||||||
|
|
||||||
|
console.log(`💰 MRR Breakdown\n`);
|
||||||
|
console.log(`Total MRR: $${totalMRR.toFixed(2)}`);
|
||||||
|
console.log(`Active Subscribers: ${data.length}\n`);
|
||||||
|
|
||||||
|
for (const [product, stats] of sorted) {
|
||||||
|
console.log(`${product}`);
|
||||||
|
console.log(` Subscribers: ${stats.count}`);
|
||||||
|
console.log(` MRR: $${stats.mrr.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function customers(limit = 20) {
|
||||||
|
const { data } = await api("/customers", { "page[size]": String(limit) });
|
||||||
|
|
||||||
|
let totalLTV = 0;
|
||||||
|
console.log(`👥 Customers (${data.length})\n`);
|
||||||
|
|
||||||
|
for (const cust of data) {
|
||||||
|
const c = cust.attributes;
|
||||||
|
const ltv = parseInt(c.total_revenue_currency || 0) / 100;
|
||||||
|
totalLTV += ltv;
|
||||||
|
console.log(
|
||||||
|
`${c.email.padEnd(35)} | LTV: $${ltv.toFixed(2).padStart(8)} | ${c.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nTotal LTV: $${totalLTV.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function products() {
|
||||||
|
const { data } = await api("/products");
|
||||||
|
|
||||||
|
console.log(`📦 Products (${data.length})\n`);
|
||||||
|
for (const prod of data) {
|
||||||
|
const p = prod.attributes;
|
||||||
|
const statusIcon = p.status === "published" ? "●" : "○";
|
||||||
|
console.log(`${statusIcon} ${p.name}`);
|
||||||
|
console.log(` Price: ${p.price_formatted || "varies"}`);
|
||||||
|
console.log(` URL: ${p.buy_now_url}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refunds(limit = 20) {
|
||||||
|
// Fetch all orders and filter for refunded ones client-side
|
||||||
|
// (LemonSqueezy API filter[refunded] is unreliable)
|
||||||
|
const { data } = await api("/orders", {
|
||||||
|
"page[size]": String(Math.min(limit * 5, 100)), // Fetch more to find refunds
|
||||||
|
});
|
||||||
|
|
||||||
|
const refunded = data.filter((o: any) =>
|
||||||
|
o.attributes.refunded || o.attributes.status === "refunded"
|
||||||
|
).slice(0, limit);
|
||||||
|
|
||||||
|
const total = refunded.reduce(
|
||||||
|
(sum: number, o: any) => sum + parseInt(o.attributes.total_usd || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`↩️ Refunds (${refunded.length})\n`);
|
||||||
|
console.log(`Total Refunded: ${cents(total)}\n`);
|
||||||
|
|
||||||
|
if (refunded.length === 0) {
|
||||||
|
console.log("No refunds found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const order of refunded) {
|
||||||
|
const a = order.attributes;
|
||||||
|
console.log(
|
||||||
|
`#${a.order_number} | ${a.total_formatted.padEnd(10)} | ${a.user_email} | ${formatDate(a.refunded_at || a.created_at)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help
|
||||||
|
function help() {
|
||||||
|
console.log(`
|
||||||
|
LemonSqueezy CLI
|
||||||
|
|
||||||
|
Usage: ls <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
overview Revenue overview (total + 30-day)
|
||||||
|
orders [--limit N] Recent orders (default: 20)
|
||||||
|
orders --status X Filter by status (paid/refunded/pending)
|
||||||
|
orders --from DATE --to DATE
|
||||||
|
Orders in date range with week-by-week summary
|
||||||
|
order <num|email> Lookup by order number or customer email
|
||||||
|
subs [--status X] Subscriptions list
|
||||||
|
mrr MRR breakdown by product
|
||||||
|
customers [--limit N] Customer list with LTV
|
||||||
|
products All products
|
||||||
|
refunds [--limit N] Refunded orders
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
LEMONSQUEEZY_API_KEY API key (required)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ls overview
|
||||||
|
ls orders --limit 10
|
||||||
|
ls orders --from 2025-11-01 --to 2026-01-20
|
||||||
|
ls order 12345
|
||||||
|
ls order customer@example.com
|
||||||
|
ls subs --status active
|
||||||
|
ls mrr
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const cmd = args[0];
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
const flags: Record<string, string> = {};
|
||||||
|
for (let i = 1; i < args.length; i++) {
|
||||||
|
if (args[i].startsWith("--")) {
|
||||||
|
const key = args[i].slice(2);
|
||||||
|
flags[key] = args[i + 1] || "true";
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (cmd) {
|
||||||
|
case "overview":
|
||||||
|
await overview();
|
||||||
|
break;
|
||||||
|
case "orders":
|
||||||
|
await orders(parseInt(flags.limit) || 100, flags.status, flags.from, flags.to);
|
||||||
|
break;
|
||||||
|
case "order":
|
||||||
|
const query = args[1];
|
||||||
|
if (!query) {
|
||||||
|
console.error("Usage: ls order <order_number|email>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await orderLookup(query);
|
||||||
|
break;
|
||||||
|
case "subs":
|
||||||
|
case "subscriptions":
|
||||||
|
await subscriptions(flags.status);
|
||||||
|
break;
|
||||||
|
case "mrr":
|
||||||
|
await mrr();
|
||||||
|
break;
|
||||||
|
case "customers":
|
||||||
|
await customers(parseInt(flags.limit) || 20);
|
||||||
|
break;
|
||||||
|
case "products":
|
||||||
|
await products();
|
||||||
|
break;
|
||||||
|
case "refunds":
|
||||||
|
await refunds(parseInt(flags.limit) || 20);
|
||||||
|
break;
|
||||||
|
case "help":
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
case undefined:
|
||||||
|
help();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Unknown command: ${cmd}`);
|
||||||
|
help();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@ -223,6 +223,7 @@ export const dispatchTelegramMessage = async ({
|
|||||||
chunkMode,
|
chunkMode,
|
||||||
onVoiceRecording: sendRecordVoice,
|
onVoiceRecording: sendRecordVoice,
|
||||||
linkPreview: telegramCfg.linkPreview,
|
linkPreview: telegramCfg.linkPreview,
|
||||||
|
notifyEmptyResponse: info.kind === "final",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
@ -484,7 +484,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
cfg,
|
cfg,
|
||||||
dispatcherOptions: {
|
dispatcherOptions: {
|
||||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||||
deliver: async (payload) => {
|
deliver: async (payload, info) => {
|
||||||
await deliverReplies({
|
await deliverReplies({
|
||||||
replies: [payload],
|
replies: [payload],
|
||||||
chatId: String(chatId),
|
chatId: String(chatId),
|
||||||
@ -497,6 +497,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
tableMode,
|
tableMode,
|
||||||
chunkMode,
|
chunkMode,
|
||||||
linkPreview: telegramCfg.linkPreview,
|
linkPreview: telegramCfg.linkPreview,
|
||||||
|
notifyEmptyResponse: info.kind === "final",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
@ -42,12 +42,15 @@ export async function deliverReplies(params: {
|
|||||||
onVoiceRecording?: () => Promise<void> | void;
|
onVoiceRecording?: () => Promise<void> | void;
|
||||||
/** Controls whether link previews are shown. Default: true (previews enabled). */
|
/** Controls whether link previews are shown. Default: true (previews enabled). */
|
||||||
linkPreview?: boolean;
|
linkPreview?: boolean;
|
||||||
}) {
|
/** If true, send a fallback message when all replies are empty. Default: false */
|
||||||
|
notifyEmptyResponse?: boolean;
|
||||||
|
}): Promise<{ delivered: boolean }> {
|
||||||
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId, linkPreview } =
|
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId, linkPreview } =
|
||||||
params;
|
params;
|
||||||
const chunkMode = params.chunkMode ?? "length";
|
const chunkMode = params.chunkMode ?? "length";
|
||||||
const threadParams = buildTelegramThreadParams(messageThreadId);
|
const threadParams = buildTelegramThreadParams(messageThreadId);
|
||||||
let hasReplied = false;
|
let hasReplied = false;
|
||||||
|
let skippedEmpty = 0;
|
||||||
const chunkText = (markdown: string) => {
|
const chunkText = (markdown: string) => {
|
||||||
const markdownChunks =
|
const markdownChunks =
|
||||||
chunkMode === "newline"
|
chunkMode === "newline"
|
||||||
@ -75,6 +78,7 @@ export async function deliverReplies(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
runtime.error?.(danger("reply missing text/media"));
|
runtime.error?.(danger("reply missing text/media"));
|
||||||
|
skippedEmpty++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId);
|
const replyToId = replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId);
|
||||||
@ -255,6 +259,18 @@ export async function deliverReplies(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If all replies were empty and notifyEmptyResponse is enabled, send a fallback message
|
||||||
|
// Check both: (1) replies with no content (skippedEmpty), (2) no replies at all (empty array)
|
||||||
|
if (!hasReplied && (skippedEmpty > 0 || replies.length === 0) && params.notifyEmptyResponse) {
|
||||||
|
const fallbackText = "No response generated. Please try again.";
|
||||||
|
await sendTelegramText(bot, chatId, fallbackText, runtime, {
|
||||||
|
messageThreadId,
|
||||||
|
});
|
||||||
|
hasReplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { delivered: hasReplied };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveMedia(
|
export async function resolveMedia(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user