@analyticscli/growth-engineer 0.1.1-preview.13 → 0.1.1-preview.15

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.
@@ -140,10 +140,18 @@ function printMessages(messages) {
140
140
  }
141
141
  }
142
142
 
143
+ function truncateDiscordText(value, maxLength) {
144
+ const text = String(value || "").trim();
145
+ if (text.length <= maxLength) {
146
+ return text;
147
+ }
148
+ return `${text.slice(0, Math.max(0, maxLength - 3)).trim()}...`;
149
+ }
150
+
143
151
  function chunkMessage(content) {
144
152
  const chunks = [];
145
153
  const maxLength = 1900;
146
- let remaining = content.trim();
154
+ let remaining = String(content || "").trim();
147
155
 
148
156
  while (remaining.length > maxLength) {
149
157
  let splitAt = remaining.lastIndexOf("\n", maxLength);
@@ -163,22 +171,194 @@ function chunkMessage(content) {
163
171
  return chunks;
164
172
  }
165
173
 
166
- function normalizeEmbedPayload(input) {
167
- if (process.env.OPENCLAW_DISCORD_DELIVERY_FORMAT !== "embed" && !process.argv.includes("--json")) {
174
+ function discordField(name, value, inline = false) {
175
+ return {
176
+ name: truncateDiscordText(name, 256) || "Detail",
177
+ value: truncateDiscordText(value, 1024) || "-",
178
+ inline,
179
+ };
180
+ }
181
+
182
+ function splitNamedLine(line) {
183
+ const clean = String(line || "").replace(/^-\s*/, "").trim();
184
+ const bracketMarker = clean.indexOf(": [");
185
+ if (bracketMarker > 0) {
186
+ return [clean.slice(0, bracketMarker).trim(), clean.slice(bracketMarker + 2).trim()];
187
+ }
188
+ const splitAt = clean.lastIndexOf(": ");
189
+ if (splitAt > 0) {
190
+ return [clean.slice(0, splitAt).trim(), clean.slice(splitAt + 2).trim()];
191
+ }
192
+ return null;
193
+ }
194
+
195
+ function buildStructuredOpenClawDailyPayload(text, lines) {
196
+ const title = truncateDiscordText(lines[0], 256);
197
+ const fields = [];
198
+ let inTopByProject = false;
199
+ let pendingFinding = null;
200
+ const flushPendingFinding = () => {
201
+ if (pendingFinding) {
202
+ fields.push(discordField(pendingFinding.name, pendingFinding.value));
203
+ pendingFinding = null;
204
+ }
205
+ };
206
+
207
+ for (const line of lines.slice(1)) {
208
+ if (/^Top by project:/i.test(line)) {
209
+ inTopByProject = true;
210
+ continue;
211
+ }
212
+ if (/^Action:/i.test(line)) {
213
+ flushPendingFinding();
214
+ fields.push(discordField("Action", line.replace(/^Action:\s*/i, ""), true));
215
+ continue;
216
+ }
217
+ if (/^Suppressed today:/i.test(line)) {
218
+ flushPendingFinding();
219
+ fields.push(discordField("Suppressed today", line.replace(/^Suppressed today:\s*/i, ""), true));
220
+ continue;
221
+ }
222
+ if (/^Charts:/i.test(line)) {
223
+ flushPendingFinding();
224
+ fields.push(discordField("Charts", line.replace(/^Charts:\s*/i, ""), true));
225
+ continue;
226
+ }
227
+ if (/^Runner completed/i.test(line)) {
228
+ flushPendingFinding();
229
+ fields.push(discordField("Run status", line));
230
+ continue;
231
+ }
232
+ if (/^Link:/i.test(line)) {
233
+ if (pendingFinding) {
234
+ pendingFinding.value = `${pendingFinding.value}\n${line}`;
235
+ } else {
236
+ fields.push(discordField("Link", line.replace(/^Link:\s*/i, "")));
237
+ }
238
+ continue;
239
+ }
240
+ if (/^\d+\s+events?,/i.test(line) && pendingFinding) {
241
+ pendingFinding.value = `${pendingFinding.value}\n${line}`;
242
+ continue;
243
+ }
244
+ if (inTopByProject && /^-\s*/.test(line)) {
245
+ flushPendingFinding();
246
+ const named = splitNamedLine(line);
247
+ if (named) {
248
+ fields.push(discordField(named[0], named[1]));
249
+ }
250
+ continue;
251
+ }
252
+ const named = splitNamedLine(line);
253
+ if (named && /^(sentry|glitchtip|analytics|github|asc|appStoreConnect|revenue|coolify|stripe|paddle)/i.test(named[0])) {
254
+ flushPendingFinding();
255
+ pendingFinding = { name: named[0], value: named[1] };
256
+ continue;
257
+ }
258
+ if (line.trim()) {
259
+ flushPendingFinding();
260
+ fields.push(discordField("Detail", line));
261
+ }
262
+ }
263
+ flushPendingFinding();
264
+
265
+ return {
266
+ content: "",
267
+ embeds: [
268
+ {
269
+ title,
270
+ color: /^OpenClaw (daily|healthcheck): OK/i.test(title) ? 0x12b76a : 0xf79009,
271
+ fields: fields.slice(0, 20),
272
+ footer: { text: "GROWTH_RUN" },
273
+ timestamp: new Date().toISOString(),
274
+ },
275
+ ],
276
+ fallbackText: text,
277
+ };
278
+ }
279
+
280
+ function buildStructuredConnectorPayload(text, lines) {
281
+ const title = truncateDiscordText(lines[0], 256);
282
+ const fields = [];
283
+ let description = "";
284
+ for (const line of lines.slice(1)) {
285
+ if (/^Secrets stay/i.test(line)) {
286
+ description = line;
287
+ continue;
288
+ }
289
+ if (/^Fix:/i.test(line)) {
290
+ fields.push(discordField("Fix", line.replace(/^Fix:\s*/i, "")));
291
+ continue;
292
+ }
293
+ if (/^At\s+\d{4}-/i.test(line)) {
294
+ fields.push(discordField("Proof", line));
295
+ continue;
296
+ }
297
+ if (/CONNECTOR_HEALTH_ALERT/i.test(line)) {
298
+ continue;
299
+ }
300
+ const named = splitNamedLine(line);
301
+ if (named) {
302
+ fields.push(discordField(named[0], named[1]));
303
+ } else if (line.trim()) {
304
+ fields.push(discordField("Detail", line));
305
+ }
306
+ }
307
+ return {
308
+ content: "",
309
+ embeds: [
310
+ {
311
+ title,
312
+ description,
313
+ color: /blocked|failed|issue/i.test(text) ? 0xd92d20 : 0xf79009,
314
+ fields: fields.slice(0, 20),
315
+ footer: { text: "CONNECTOR_HEALTH_ALERT" },
316
+ timestamp: new Date().toISOString(),
317
+ },
318
+ ],
319
+ fallbackText: text,
320
+ };
321
+ }
322
+
323
+ function structuredTextToEmbedPayload(input) {
324
+ const text = String(input || "").trim();
325
+ if (!text) {
168
326
  return null;
169
327
  }
170
- try {
171
- const payload = JSON.parse(String(input || ""));
172
- const embeds = Array.isArray(payload.embeds) ? payload.embeds : [];
173
- if (embeds.length === 0) return null;
174
- return {
175
- content: String(payload.content || "").slice(0, 2000),
176
- embeds: embeds.slice(0, 10),
177
- fallbackText: String(payload.fallbackText || payload.fallback_text || "").trim(),
178
- };
179
- } catch {
328
+
329
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
330
+ if (lines.length === 0) {
180
331
  return null;
181
332
  }
333
+ if (/^OpenClaw connector health:/i.test(lines[0]) || /CONNECTOR_HEALTH_ALERT/.test(text)) {
334
+ return buildStructuredConnectorPayload(text, lines);
335
+ }
336
+ if (/^OpenClaw (daily|healthcheck)(:|\s)/i.test(lines[0])) {
337
+ return buildStructuredOpenClawDailyPayload(text, lines);
338
+ }
339
+ return null;
340
+ }
341
+
342
+ function normalizeEmbedPayload(input) {
343
+ const raw = String(input || "");
344
+ if (process.env.OPENCLAW_DISCORD_DELIVERY_FORMAT === "embed" || process.argv.includes("--json") || raw.trim().startsWith("{")) {
345
+ try {
346
+ const payload = JSON.parse(raw);
347
+ const embeds = Array.isArray(payload.embeds) ? payload.embeds : [];
348
+ if (embeds.length > 0) {
349
+ return {
350
+ content: String(payload.content || "").slice(0, 2000),
351
+ embeds: embeds.slice(0, 10),
352
+ fallbackText: String(payload.fallbackText || payload.fallback_text || "").trim(),
353
+ };
354
+ }
355
+ } catch {
356
+ if (process.argv.includes("--json")) {
357
+ return null;
358
+ }
359
+ }
360
+ }
361
+ return structuredTextToEmbedPayload(raw);
182
362
  }
183
363
 
184
364
  async function sendDiscordPayload(payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@analyticscli/growth-engineer",
3
- "version": "0.1.1-preview.13",
3
+ "version": "0.1.1-preview.15",
4
4
  "description": "Growth Engineer CLI for connector setup, scheduling, health checks, and OpenClaw-compatible growth runs.",
5
5
  "license": "MIT",
6
6
  "type": "module",