@analyticscli/growth-engineer 0.1.1-preview.3 → 0.1.1-preview.30
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.
- package/dist/index.js +52 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/discord-openclaw-bridge.mjs +489 -0
- package/dist/runtime/export-asc-summary.mjs +255 -104
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/export-paddle-summary.mjs +73 -19
- package/dist/runtime/export-paddle-summary.mjs.map +1 -1
- package/dist/runtime/openclaw-exporters-lib.d.mts +79 -2
- package/dist/runtime/openclaw-exporters-lib.mjs +231 -68
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +84 -20
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +154 -17
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.mjs +2 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +170 -33
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +183 -2
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +637 -183
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
const DISCORD_API_BASE_URL = "https://discord.com/api/v10";
|
|
5
|
+
const ALLOWED_GUILD_ID = "1431275274770845708";
|
|
6
|
+
const OPENCLAW_CHANNEL_ID = "1471908112100495617";
|
|
7
|
+
const HERMES_CHANNEL_ID = "1503376909977780414";
|
|
8
|
+
const OPENCLAW_BOT_ID = "1471692354187559134";
|
|
9
|
+
const PRIMARY_TARGET_LABEL = "openclaw";
|
|
10
|
+
const MIN_ASK_TIMEOUT_SECONDS = 600;
|
|
11
|
+
const DISCORD_TARGETS = [
|
|
12
|
+
{
|
|
13
|
+
label: PRIMARY_TARGET_LABEL,
|
|
14
|
+
guildId: ALLOWED_GUILD_ID,
|
|
15
|
+
channelId: OPENCLAW_CHANNEL_ID,
|
|
16
|
+
allowedMentionUsers: [OPENCLAW_BOT_ID],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "hermes",
|
|
20
|
+
guildId: ALLOWED_GUILD_ID,
|
|
21
|
+
channelId: HERMES_CHANNEL_ID,
|
|
22
|
+
allowedMentionUsers: [],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const usage = `Usage:
|
|
27
|
+
node scripts/discord-openclaw-bridge.mjs read [--limit 20]
|
|
28
|
+
node scripts/discord-openclaw-bridge.mjs send "message"
|
|
29
|
+
node scripts/discord-openclaw-bridge.mjs send --stdin
|
|
30
|
+
node scripts/discord-openclaw-bridge.mjs watch [--interval 5] [--limit 10]
|
|
31
|
+
node scripts/discord-openclaw-bridge.mjs ask "message" [--timeout 600]
|
|
32
|
+
|
|
33
|
+
Required environment:
|
|
34
|
+
DISCORD_BOT_TOKEN
|
|
35
|
+
|
|
36
|
+
Hard-locked target:
|
|
37
|
+
guild ${ALLOWED_GUILD_ID}
|
|
38
|
+
channels ${DISCORD_TARGETS.map((target) => `${target.label}:${target.channelId}`).join(", ")}`;
|
|
39
|
+
|
|
40
|
+
function readFlag(args, name, fallback) {
|
|
41
|
+
const index = args.indexOf(name);
|
|
42
|
+
if (index === -1) {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
const value = args[index + 1];
|
|
46
|
+
if (!value || value.startsWith("--")) {
|
|
47
|
+
throw new Error(`Missing value for ${name}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasFlag(args, name) {
|
|
53
|
+
return args.includes(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asPositiveInteger(value, label) {
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
59
|
+
throw new Error(`${label} must be a positive integer`);
|
|
60
|
+
}
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getToken() {
|
|
65
|
+
const token = process.env.DISCORD_BOT_TOKEN?.trim();
|
|
66
|
+
if (!token) {
|
|
67
|
+
throw new Error("DISCORD_BOT_TOKEN is required. Put it in .env or export it in your shell.");
|
|
68
|
+
}
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function discordFetch(path, options = {}) {
|
|
73
|
+
const response = await fetch(`${DISCORD_API_BASE_URL}${path}`, {
|
|
74
|
+
...options,
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bot ${getToken()}`,
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
...options.headers,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (response.status === 429) {
|
|
83
|
+
const retry = await response.json().catch(() => ({}));
|
|
84
|
+
const retryAfter = Number(retry.retry_after ?? 1);
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, Math.ceil(retryAfter * 1000)));
|
|
86
|
+
return discordFetch(path, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
throw new Error(`Discord API ${response.status} for ${path}: ${text}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (response.status === 204) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return response.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getPrimaryTarget() {
|
|
101
|
+
return DISCORD_TARGETS.find((target) => target.label === PRIMARY_TARGET_LABEL) ?? DISCORD_TARGETS[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function validateTargetChannel(target) {
|
|
105
|
+
const channel = await discordFetch(`/channels/${target.channelId}`);
|
|
106
|
+
if (channel.guild_id !== target.guildId) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Refusing to use ${target.label} channel ${target.channelId}; Discord returned guild ${channel.guild_id ?? "unknown"}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return channel;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatMessage(message) {
|
|
115
|
+
const author = message.author?.bot
|
|
116
|
+
? `${message.author.username} [bot]`
|
|
117
|
+
: (message.author?.global_name ?? message.author?.username ?? "unknown");
|
|
118
|
+
const timestamp = new Date(message.timestamp).toISOString();
|
|
119
|
+
const content = message.content?.trim() ? message.content.trim() : "(no text content)";
|
|
120
|
+
const attachments = Array.isArray(message.attachments) && message.attachments.length > 0
|
|
121
|
+
? `\n attachments: ${message.attachments.map((item) => item.url).join(", ")}`
|
|
122
|
+
: "";
|
|
123
|
+
return `[${timestamp}] ${author} (${message.id})\n ${content}${attachments}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function readMessages({ limit, after }) {
|
|
127
|
+
const target = getPrimaryTarget();
|
|
128
|
+
await validateTargetChannel(target);
|
|
129
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
130
|
+
if (after) {
|
|
131
|
+
params.set("after", after);
|
|
132
|
+
}
|
|
133
|
+
const messages = await discordFetch(`/channels/${target.channelId}/messages?${params}`);
|
|
134
|
+
return messages.reverse();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function printMessages(messages) {
|
|
138
|
+
for (const message of messages) {
|
|
139
|
+
console.log(formatMessage(message));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
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
|
+
|
|
151
|
+
function chunkMessage(content) {
|
|
152
|
+
const chunks = [];
|
|
153
|
+
const maxLength = 1900;
|
|
154
|
+
let remaining = String(content || "").trim();
|
|
155
|
+
|
|
156
|
+
while (remaining.length > maxLength) {
|
|
157
|
+
let splitAt = remaining.lastIndexOf("\n", maxLength);
|
|
158
|
+
if (splitAt < maxLength * 0.5) {
|
|
159
|
+
splitAt = remaining.lastIndexOf(" ", maxLength);
|
|
160
|
+
}
|
|
161
|
+
if (splitAt < maxLength * 0.5) {
|
|
162
|
+
splitAt = maxLength;
|
|
163
|
+
}
|
|
164
|
+
chunks.push(remaining.slice(0, splitAt).trim());
|
|
165
|
+
remaining = remaining.slice(splitAt).trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (remaining) {
|
|
169
|
+
chunks.push(remaining);
|
|
170
|
+
}
|
|
171
|
+
return chunks;
|
|
172
|
+
}
|
|
173
|
+
|
|
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) {
|
|
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
|
+
|
|
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);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function sendDiscordPayload(payload) {
|
|
365
|
+
const sent = [];
|
|
366
|
+
for (const target of DISCORD_TARGETS) {
|
|
367
|
+
await validateTargetChannel(target);
|
|
368
|
+
const message = await discordFetch(`/channels/${target.channelId}/messages`, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
body: JSON.stringify({
|
|
371
|
+
allowed_mentions: { parse: [], users: target.allowedMentionUsers },
|
|
372
|
+
content: payload.content || "",
|
|
373
|
+
embeds: payload.embeds,
|
|
374
|
+
}),
|
|
375
|
+
});
|
|
376
|
+
sent.push({ target, message });
|
|
377
|
+
}
|
|
378
|
+
return sent;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function sendMessage(content) {
|
|
382
|
+
const embedPayload = normalizeEmbedPayload(content);
|
|
383
|
+
if (embedPayload) {
|
|
384
|
+
return await sendDiscordPayload(embedPayload);
|
|
385
|
+
}
|
|
386
|
+
|
|
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;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function readStdin() {
|
|
410
|
+
const chunks = [];
|
|
411
|
+
for await (const chunk of process.stdin) {
|
|
412
|
+
chunks.push(Buffer.from(chunk));
|
|
413
|
+
}
|
|
414
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function watchMessages({ intervalSeconds, limit, timeoutSeconds }) {
|
|
418
|
+
const initial = await readMessages({ limit });
|
|
419
|
+
printMessages(initial);
|
|
420
|
+
|
|
421
|
+
let newestId = initial.at(-1)?.id;
|
|
422
|
+
const startTime = Date.now();
|
|
423
|
+
|
|
424
|
+
while (true) {
|
|
425
|
+
if (timeoutSeconds && Date.now() - startTime > timeoutSeconds * 1000) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
|
|
430
|
+
const messages = await readMessages({ limit: 50, after: newestId });
|
|
431
|
+
if (messages.length === 0) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
printMessages(messages);
|
|
435
|
+
newestId = messages.at(-1).id;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function main() {
|
|
440
|
+
const rawArgs = process.argv.slice(2);
|
|
441
|
+
if (rawArgs[0] === "--") {
|
|
442
|
+
rawArgs.shift();
|
|
443
|
+
}
|
|
444
|
+
const [command, ...args] = rawArgs;
|
|
445
|
+
if (!command || command === "--help" || command === "-h") {
|
|
446
|
+
console.log(usage);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (command === "read") {
|
|
451
|
+
const limit = asPositiveInteger(readFlag(args, "--limit", "20"), "--limit");
|
|
452
|
+
const messages = await readMessages({ limit });
|
|
453
|
+
printMessages(messages);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (command === "send") {
|
|
458
|
+
const content = hasFlag(args, "--stdin") ? await readStdin() : args.join(" ");
|
|
459
|
+
const sent = await sendMessage(content);
|
|
460
|
+
for (const { target, message } of sent) {
|
|
461
|
+
console.log(`sent ${target.label} ${message.id}`);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (command === "watch") {
|
|
467
|
+
const intervalSeconds = asPositiveInteger(readFlag(args, "--interval", "5"), "--interval");
|
|
468
|
+
const limit = asPositiveInteger(readFlag(args, "--limit", "10"), "--limit");
|
|
469
|
+
await watchMessages({ intervalSeconds, limit });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (command === "ask") {
|
|
474
|
+
const requestedTimeoutSeconds = asPositiveInteger(readFlag(args, "--timeout", String(MIN_ASK_TIMEOUT_SECONDS)), "--timeout");
|
|
475
|
+
const timeoutSeconds = Math.max(requestedTimeoutSeconds, MIN_ASK_TIMEOUT_SECONDS);
|
|
476
|
+
const filteredArgs = args.filter((arg, index) => arg !== "--timeout" && args[index - 1] !== "--timeout");
|
|
477
|
+
const sent = await sendMessage(filteredArgs.join(" "));
|
|
478
|
+
console.log(`sent ${sent.map(({ target, message }) => `${target.label}:${message.id}`).join(", ")}`);
|
|
479
|
+
await watchMessages({ intervalSeconds: 5, limit: 1, timeoutSeconds });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
throw new Error(`Unknown command: ${command}\n\n${usage}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
main().catch((error) => {
|
|
487
|
+
console.error(error.message);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
});
|