@analyticscli/growth-engineer 0.1.1-preview.14 → 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.
@@ -148,12 +148,12 @@ function truncateDiscordText(value, maxLength) {
148
148
  return `${text.slice(0, Math.max(0, maxLength - 3)).trim()}...`;
149
149
  }
150
150
 
151
- function chunkEmbedDescription(content) {
151
+ function chunkMessage(content) {
152
152
  const chunks = [];
153
- const maxLength = 4000;
153
+ const maxLength = 1900;
154
154
  let remaining = String(content || "").trim();
155
155
 
156
- while (remaining.length > maxLength && chunks.length < 9) {
156
+ while (remaining.length > maxLength) {
157
157
  let splitAt = remaining.lastIndexOf("\n", maxLength);
158
158
  if (splitAt < maxLength * 0.5) {
159
159
  splitAt = remaining.lastIndexOf(" ", maxLength);
@@ -166,49 +166,179 @@ function chunkEmbedDescription(content) {
166
166
  }
167
167
 
168
168
  if (remaining) {
169
- chunks.push(truncateDiscordText(remaining, maxLength));
169
+ chunks.push(remaining);
170
170
  }
171
- return chunks.slice(0, 10);
171
+ return chunks;
172
172
  }
173
173
 
174
- function plainTextToEmbedPayload(input) {
175
- const text = String(input || "").trim();
176
- if (!text) {
177
- return null;
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()];
178
191
  }
192
+ return null;
193
+ }
179
194
 
180
- const lines = text.split(/\r?\n/);
181
- const firstLineIndex = Math.max(0, lines.findIndex((line) => line.trim()));
182
- const firstLine = lines[firstLineIndex]?.trim() || "OpenClaw update";
183
- const title = truncateDiscordText(firstLine, 256);
184
- const body = lines.slice(firstLineIndex + 1).join("\n").trim();
185
- const descriptionChunks = chunkEmbedDescription(body || (firstLine.length > 256 ? firstLine : ""));
186
- const embeds = [];
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
+ };
187
206
 
188
- if (descriptionChunks.length === 0) {
189
- embeds.push({
190
- title,
191
- color: 0x2f81f7,
192
- timestamp: new Date().toISOString(),
193
- });
194
- } else {
195
- for (const [index, description] of descriptionChunks.entries()) {
196
- embeds.push({
197
- ...(index === 0 ? { title } : {}),
198
- description,
199
- color: 0x2f81f7,
200
- timestamp: new Date().toISOString(),
201
- });
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));
202
261
  }
203
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
+ }
204
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
+ }
205
307
  return {
206
308
  content: "",
207
- embeds,
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
+ ],
208
319
  fallbackText: text,
209
320
  };
210
321
  }
211
322
 
323
+ function structuredTextToEmbedPayload(input) {
324
+ const text = String(input || "").trim();
325
+ if (!text) {
326
+ return null;
327
+ }
328
+
329
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
330
+ if (lines.length === 0) {
331
+ return null;
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
+
212
342
  function normalizeEmbedPayload(input) {
213
343
  const raw = String(input || "");
214
344
  if (process.env.OPENCLAW_DISCORD_DELIVERY_FORMAT === "embed" || process.argv.includes("--json") || raw.trim().startsWith("{")) {
@@ -228,7 +358,7 @@ function normalizeEmbedPayload(input) {
228
358
  }
229
359
  }
230
360
  }
231
- return plainTextToEmbedPayload(raw);
361
+ return structuredTextToEmbedPayload(raw);
232
362
  }
233
363
 
234
364
  async function sendDiscordPayload(payload) {
@@ -254,7 +384,26 @@ async function sendMessage(content) {
254
384
  return await sendDiscordPayload(embedPayload);
255
385
  }
256
386
 
257
- throw new Error("Refusing to send an empty message.");
387
+ const chunks = chunkMessage(content);
388
+ if (chunks.length === 0) {
389
+ throw new Error("Refusing to send an empty message.");
390
+ }
391
+
392
+ const sent = [];
393
+ for (const target of DISCORD_TARGETS) {
394
+ await validateTargetChannel(target);
395
+ for (const chunk of chunks) {
396
+ const message = await discordFetch(`/channels/${target.channelId}/messages`, {
397
+ method: "POST",
398
+ body: JSON.stringify({
399
+ allowed_mentions: { parse: [], users: target.allowedMentionUsers },
400
+ content: chunk,
401
+ }),
402
+ });
403
+ sent.push({ target, message });
404
+ }
405
+ }
406
+ return sent;
258
407
  }
259
408
 
260
409
  async function readStdin() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@analyticscli/growth-engineer",
3
- "version": "0.1.1-preview.14",
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",