@analyticscli/growth-engineer 0.1.1-preview.7 → 0.1.1-preview.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -458,6 +458,7 @@ function runShellCommand(command, timeoutMs = 120_000, options = {}) {
458
458
  cwd: options.cwd,
459
459
  env: {
460
460
  ...process.env,
461
+ ...(options.env || {}),
461
462
  DEBIAN_FRONTEND: 'noninteractive',
462
463
  SUDO_ASKPASS: '/bin/false',
463
464
  SUDO_PROMPT: '',
@@ -900,6 +901,8 @@ function notificationChannelKey(channel) {
900
901
  return `slack:${channel?.label || channel?.webhookEnv || 'slack'}`;
901
902
  if (type === 'webhook')
902
903
  return `webhook:${channel?.label || channel?.urlEnv || channel?.webhookEnv || 'webhook'}`;
904
+ if (type === 'discord')
905
+ return `discord:${channel?.label || channel?.command || 'discord'}`;
903
906
  if (type === 'command')
904
907
  return `command:${channel?.label || channel?.command || 'command'}`;
905
908
  return `${type}:${channel?.label || type}`;
@@ -956,7 +959,7 @@ function getDeliveryNotificationChannels(config, kind) {
956
959
  }
957
960
  if (deliveries.discord?.enabled) {
958
961
  channels.push({
959
- type: 'command',
962
+ type: 'discord',
960
963
  label: deliveries.discord.label || 'discord',
961
964
  command: deliveries.discord.command || '',
962
965
  });
@@ -1050,7 +1053,23 @@ async function sendCommandConnectorHealthAlert(channel, message) {
1050
1053
  sent: result.ok,
1051
1054
  external: true,
1052
1055
  target: channel.label || 'command',
1053
- detail: result.ok ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
1056
+ detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
1057
+ };
1058
+ }
1059
+ async function sendDiscordConnectorHealthAlert(channel, message, statusPayload, unhealthyConnectors, fingerprint) {
1060
+ if (!channel.command) {
1061
+ return { sent: false, target: channel.label || 'discord', detail: 'discord command not configured' };
1062
+ }
1063
+ const payload = buildDiscordConnectorHealthPayload(message, statusPayload, unhealthyConnectors, fingerprint);
1064
+ const result = await runShellCommand(String(channel.command), 60_000, {
1065
+ input: JSON.stringify(payload),
1066
+ env: { OPENCLAW_DISCORD_DELIVERY_FORMAT: 'embed' },
1067
+ });
1068
+ return {
1069
+ sent: result.ok,
1070
+ external: true,
1071
+ target: channel.label || 'discord',
1072
+ detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
1054
1073
  };
1055
1074
  }
1056
1075
  function hasExternalNotificationChannel(channels) {
@@ -1059,6 +1078,57 @@ function hasExternalNotificationChannel(channels) {
1059
1078
  function hasSuccessfulExternalDelivery(results) {
1060
1079
  return results.some((result) => result?.sent === true && result?.external === true);
1061
1080
  }
1081
+ function discordTruncate(value, maxLength) {
1082
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
1083
+ if (text.length <= maxLength)
1084
+ return text;
1085
+ return `${text.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
1086
+ }
1087
+ function discordField(name, value, inline = false) {
1088
+ return {
1089
+ name: discordTruncate(name, 256) || 'Detail',
1090
+ value: discordTruncate(value, 1024) || '-',
1091
+ inline,
1092
+ };
1093
+ }
1094
+ function connectorStatusColor(unhealthyConnectors) {
1095
+ return unhealthyConnectors.some((entry) => String(entry?.status || '').toLowerCase() === 'blocked')
1096
+ ? 0xd92d20
1097
+ : 0xf79009;
1098
+ }
1099
+ function buildDiscordConnectorHealthPayload(message, statusPayload, unhealthyConnectors, fingerprint) {
1100
+ const fields = unhealthyConnectors.slice(0, 10).map((entry) => {
1101
+ const command = buildConnectorWizardCommand(statusPayload?.configPath || DEFAULT_CONFIG_PATH, entry);
1102
+ const parts = [
1103
+ `Status: ${entry.status || 'blocked'}`,
1104
+ conciseConnectorDetail(entry),
1105
+ command ? `Fix: \`${command}\`` : null,
1106
+ isAscWebAuthIssue(entry)
1107
+ ? 'ASC web-auth only: `ASC_WEB_APPLE_ID="<apple-id>" asc web auth login --apple-id "$ASC_WEB_APPLE_ID"`'
1108
+ : null,
1109
+ ].filter(Boolean);
1110
+ return discordField(humanConnectorName(entry.key), parts.join('\n'));
1111
+ });
1112
+ if (unhealthyConnectors.length > 10) {
1113
+ fields.push(discordField('More issues', `${unhealthyConnectors.length - 10} additional connector(s) need attention.`));
1114
+ }
1115
+ return {
1116
+ content: '',
1117
+ embeds: [
1118
+ {
1119
+ title: `OpenClaw connector health: ${unhealthyConnectors.length} issue(s)`,
1120
+ description: 'Secrets stay in the host terminal or secret store.',
1121
+ color: connectorStatusColor(unhealthyConnectors),
1122
+ fields,
1123
+ footer: {
1124
+ text: `CONNECTOR_HEALTH_ALERT • ${String(fingerprint || '').slice(0, 12)}`,
1125
+ },
1126
+ timestamp: statusPayload?.generatedAt || new Date().toISOString(),
1127
+ },
1128
+ ],
1129
+ fallbackText: message,
1130
+ };
1131
+ }
1062
1132
  function truncateMessageText(value, maxLength = 96) {
1063
1133
  const text = String(value || '').replace(/\s+/g, ' ').trim();
1064
1134
  if (text.length <= maxLength)
@@ -1428,6 +1498,9 @@ async function deliverConnectorHealthAlert({ config, configPath, message, status
1428
1498
  else if (channel.type === 'webhook') {
1429
1499
  results.push(await sendWebhookConnectorHealthAlert(channel, message, statusPayload, unhealthyConnectors, fingerprint));
1430
1500
  }
1501
+ else if (channel.type === 'discord') {
1502
+ results.push(await sendDiscordConnectorHealthAlert(channel, message, statusPayload, unhealthyConnectors, fingerprint));
1503
+ }
1431
1504
  else if (channel.type === 'command') {
1432
1505
  results.push(await sendCommandConnectorHealthAlert(channel, message));
1433
1506
  }
@@ -1557,6 +1630,67 @@ function buildGrowthRunSummaryMessage({ issuesPayload, activeCadences, sourceFil
1557
1630
  : 'No secrets were included.');
1558
1631
  return `${lines.join('\n')}\n`;
1559
1632
  }
1633
+ function growthRunTitle(activeCadences) {
1634
+ if (isShortOperationalCadence(activeCadences)) {
1635
+ return activeCadences.some((cadence) => String(cadence?.key) === 'healthcheck')
1636
+ ? 'OpenClaw healthcheck'
1637
+ : 'OpenClaw daily';
1638
+ }
1639
+ if (isDeepAnalysisCadence(activeCadences))
1640
+ return 'OpenClaw growth review';
1641
+ return 'OpenClaw growth run';
1642
+ }
1643
+ function buildDiscordGrowthRunPayload(message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts = []) {
1644
+ const issues = Array.isArray(issuesPayload?.issues) ? issuesPayload.issues : [];
1645
+ const issueCount = Number(issuesPayload?.issue_count || 0);
1646
+ const fields = [
1647
+ discordField('Cadence', activeCadences.length > 0
1648
+ ? activeCadences.map((cadence) => cadence.title || cadence.key).join(', ')
1649
+ : 'ad-hoc growth pass', false),
1650
+ discordField('Sources', Object.keys(sourceFiles || {}).sort().join(', ') || 'none', true),
1651
+ discordField('Findings', String(issueCount), true),
1652
+ ];
1653
+ if (createdGitHubArtifact) {
1654
+ fields.push(discordField('Action', 'GitHub artifact creation was attempted.', true));
1655
+ }
1656
+ const suppressedIssueCount = Number(issuesPayload?.suppressed_issue_count || 0);
1657
+ if (suppressedIssueCount > 0) {
1658
+ fields.push(discordField('Suppressed today', `${suppressedIssueCount} previously reported finding(s).`, true));
1659
+ }
1660
+ if (charts.length > 0) {
1661
+ fields.push(discordField('Charts', String(charts.length), true));
1662
+ }
1663
+ const groupedIssues = isShortOperationalCadence(activeCadences)
1664
+ ? groupIssuesByProject(issues, 4).map(([project, projectIssues]) => ({
1665
+ name: project,
1666
+ value: projectIssues.map((issue) => formatIssueSummaryLine(issue, 84)).filter(Boolean).join('\n'),
1667
+ }))
1668
+ : issues.slice(0, isDeepAnalysisCadence(activeCadences) ? 5 : 3).map((issue) => ({
1669
+ name: `${issue.priority || 'medium'} • ${issue.area || 'general'}`,
1670
+ value: formatIssueSummaryLine(issue, 96),
1671
+ }));
1672
+ for (const entry of groupedIssues) {
1673
+ if (entry.value)
1674
+ fields.push(discordField(entry.name, entry.value));
1675
+ }
1676
+ const summary = String(issuesPayload?.summary || '').trim();
1677
+ return {
1678
+ content: '',
1679
+ embeds: [
1680
+ {
1681
+ title: `${growthRunTitle(activeCadences)}: ${issueCount > 0 ? `${issueCount} finding(s)` : 'OK'}`,
1682
+ description: discordTruncate(summary || 'No secrets were included.', 500),
1683
+ color: issueCount > 0 ? 0xf79009 : 0x12b76a,
1684
+ fields: fields.slice(0, 20),
1685
+ footer: {
1686
+ text: `GROWTH_RUN • ${String(fingerprint || '').slice(0, 12)}`,
1687
+ },
1688
+ timestamp: new Date().toISOString(),
1689
+ },
1690
+ ],
1691
+ fallbackText: message,
1692
+ };
1693
+ }
1560
1694
  async function writeConfiguredOpenClawChatGrowthSummary(configPath, channel, message, issuesPayload, activeCadences, fingerprint, charts) {
1561
1695
  const markdownPath = resolveOpenClawChatDeliveryPath(channel.markdownPath, '.openclaw/chat/growth-summary.md');
1562
1696
  const jsonPath = resolveOpenClawChatDeliveryPath(channel.jsonPath, '.openclaw/chat/growth-summary.json');
@@ -1646,7 +1780,23 @@ async function sendCommandGrowthSummary(channel, message) {
1646
1780
  sent: result.ok,
1647
1781
  external: true,
1648
1782
  target: channel.label || 'command',
1649
- detail: result.ok ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
1783
+ detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
1784
+ };
1785
+ }
1786
+ async function sendDiscordGrowthSummary(channel, message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts) {
1787
+ if (!channel.command) {
1788
+ return { sent: false, target: channel.label || 'discord', detail: 'discord command not configured' };
1789
+ }
1790
+ const payload = buildDiscordGrowthRunPayload(message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts);
1791
+ const result = await runShellCommand(String(channel.command), 60_000, {
1792
+ input: JSON.stringify(payload),
1793
+ env: { OPENCLAW_DISCORD_DELIVERY_FORMAT: 'embed' },
1794
+ });
1795
+ return {
1796
+ sent: result.ok,
1797
+ external: true,
1798
+ target: channel.label || 'discord',
1799
+ detail: result.ok ? 'sent' : result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`,
1650
1800
  };
1651
1801
  }
1652
1802
  async function deliverGrowthRunSummary({ config, configPath, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, chartManifestPath, }) {
@@ -1677,6 +1827,9 @@ async function deliverGrowthRunSummary({ config, configPath, issuesPayload, acti
1677
1827
  else if (channel.type === 'webhook') {
1678
1828
  results.push(await sendWebhookGrowthSummary(channel, message, issuesPayload, activeCadences, fingerprint, charts));
1679
1829
  }
1830
+ else if (channel.type === 'discord') {
1831
+ results.push(await sendDiscordGrowthSummary(channel, message, issuesPayload, activeCadences, sourceFiles, fingerprint, createdGitHubArtifact, charts));
1832
+ }
1680
1833
  else if (channel.type === 'command') {
1681
1834
  results.push(await sendCommandGrowthSummary(channel, message));
1682
1835
  }