@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
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
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",
|