@deckasoft/waify 0.6.2 → 0.7.0
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/README.md +3 -1
- package/dist/cli/index.js +687 -230
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var init_paths = __esm({
|
|
|
33
33
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
34
34
|
import { dirname } from "path";
|
|
35
35
|
import { z } from "zod";
|
|
36
|
-
var RecipientSchema, ConfigSchema, parseConfig, defaultConfig, loadConfig, saveConfig, assertConfigReady;
|
|
36
|
+
var RecipientSchema, LANGUAGES, supportedTimezones, detectTimezone, isValidTimezone, ConfigSchema, parseConfig, defaultConfig, loadConfig, saveConfig, assertConfigReady;
|
|
37
37
|
var init_config = __esm({
|
|
38
38
|
"src/core/config.ts"() {
|
|
39
39
|
"use strict";
|
|
@@ -42,11 +42,23 @@ var init_config = __esm({
|
|
|
42
42
|
chatId: z.string(),
|
|
43
43
|
name: z.string().optional()
|
|
44
44
|
});
|
|
45
|
+
LANGUAGES = ["Spanish", "English", "Portuguese", "French", "German", "Italian"];
|
|
46
|
+
supportedTimezones = () => {
|
|
47
|
+
const supportedValuesOf = Intl.supportedValuesOf;
|
|
48
|
+
const zones = supportedValuesOf ? supportedValuesOf("timeZone") : [];
|
|
49
|
+
return zones.includes("UTC") ? zones : ["UTC", ...zones];
|
|
50
|
+
};
|
|
51
|
+
detectTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
52
|
+
isValidTimezone = (tz) => supportedTimezones().includes(tz);
|
|
45
53
|
ConfigSchema = z.object({
|
|
46
54
|
openwaBaseUrl: z.string().url().default("http://localhost:2785"),
|
|
47
55
|
openwaSessionId: z.string().nullable().default(null),
|
|
48
56
|
openwaApiKey: z.string().nullable().default(null),
|
|
49
|
-
recipients: z.array(RecipientSchema).max(1).default([])
|
|
57
|
+
recipients: z.array(RecipientSchema).max(1).default([]),
|
|
58
|
+
// Human language name (e.g. 'Spanish') injected into the generation prompt.
|
|
59
|
+
language: z.string().min(1).default("Spanish"),
|
|
60
|
+
// IANA timezone the Ofelia scheduler evaluates cron in (via the TZ env).
|
|
61
|
+
timezone: z.string().min(1).refine(isValidTimezone, { message: "timezone must be a valid IANA zone (e.g. America/Guayaquil)" }).default("UTC")
|
|
50
62
|
});
|
|
51
63
|
parseConfig = (raw) => {
|
|
52
64
|
const source = typeof raw === "object" && raw !== null ? raw : {};
|
|
@@ -92,7 +104,7 @@ var init_prompt = __esm({
|
|
|
92
104
|
defaultPrompt = {
|
|
93
105
|
systemPrompt: [
|
|
94
106
|
"You write short WhatsApp messages from a man to his partner to brighten her day.",
|
|
95
|
-
"Write
|
|
107
|
+
"Write the way a real person texts \u2014 casual and natural, not a greeting card.",
|
|
96
108
|
"Keep it to 1\u20133 sentences. Vary the mood each time: sometimes sweet, sometimes a low-key hype, sometimes playful or funny, sometimes a quick inside-joke vibe, sometimes a chill inspirational nudge.",
|
|
97
109
|
"Never sound formal, poetic, or like a motivational poster.",
|
|
98
110
|
"",
|
|
@@ -118,7 +130,11 @@ var init_prompt = __esm({
|
|
|
118
130
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
119
131
|
writeFileSync2(path, JSON.stringify(prompt, null, 2) + "\n", "utf-8");
|
|
120
132
|
};
|
|
121
|
-
generateMessage = async ({
|
|
133
|
+
generateMessage = async ({
|
|
134
|
+
provider,
|
|
135
|
+
prompt,
|
|
136
|
+
language
|
|
137
|
+
}) => provider.generateMessage({ systemPrompt: prompt.systemPrompt, examples: prompt.examples, language });
|
|
122
138
|
}
|
|
123
139
|
});
|
|
124
140
|
|
|
@@ -126,7 +142,7 @@ var init_prompt = __esm({
|
|
|
126
142
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
127
143
|
import { dirname as dirname3 } from "path";
|
|
128
144
|
import { z as z3 } from "zod";
|
|
129
|
-
var CRON_RANGES, STEP_RE, RANGE_RE, isValidCronField, isValidCron, ScheduledJobSchema, ScheduleSchema, defaultSchedule, scheduleJsonPath2, loadSchedule, saveSchedule, ofeliaRuntime, renderEnv, renderJob, renderOfeliaIni, regenerateOfeliaIni, addJob, removeJob;
|
|
145
|
+
var CRON_RANGES, STEP_RE, RANGE_RE, isValidCronAtom, isValidCronField, isValidCron, DAY_LABELS, DAY_LOOKUP, parseDays, FREQUENCIES, CronBuilderSchema, dowField, buildCron, ScheduledJobSchema, ScheduleSchema, defaultSchedule, scheduleJsonPath2, loadSchedule, saveSchedule, ofeliaRuntime, renderEnv, renderJob, renderOfeliaIni, regenerateOfeliaIni, addJob, removeJob;
|
|
130
146
|
var init_schedule = __esm({
|
|
131
147
|
"src/core/schedule.ts"() {
|
|
132
148
|
"use strict";
|
|
@@ -147,16 +163,60 @@ var init_schedule = __esm({
|
|
|
147
163
|
];
|
|
148
164
|
STEP_RE = /^(\*|\d+)\/\d+$/;
|
|
149
165
|
RANGE_RE = /^\d+-\d+$/;
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
if (STEP_RE.test(
|
|
153
|
-
const n = Number(
|
|
166
|
+
isValidCronAtom = (atom, [min, max]) => {
|
|
167
|
+
if (atom === "*") return true;
|
|
168
|
+
if (STEP_RE.test(atom) || RANGE_RE.test(atom)) return true;
|
|
169
|
+
const n = Number(atom);
|
|
154
170
|
return Number.isInteger(n) && n >= min && n <= max;
|
|
155
171
|
};
|
|
172
|
+
isValidCronField = (field, range) => {
|
|
173
|
+
const atoms = field.split(",");
|
|
174
|
+
return atoms.length > 0 && atoms.every((a) => a.length > 0 && isValidCronAtom(a, range));
|
|
175
|
+
};
|
|
156
176
|
isValidCron = (value) => {
|
|
157
177
|
const fields = value.trim().split(/\s+/);
|
|
158
178
|
return fields.length === 6 && fields.every((f, i) => isValidCronField(f, CRON_RANGES[i]));
|
|
159
179
|
};
|
|
180
|
+
DAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
181
|
+
DAY_LOOKUP = Object.fromEntries(
|
|
182
|
+
DAY_LABELS.map((label, i) => [label.toLowerCase(), i])
|
|
183
|
+
);
|
|
184
|
+
parseDays = (input) => {
|
|
185
|
+
const tokens = input.split(",").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0);
|
|
186
|
+
if (tokens.length === 0) return null;
|
|
187
|
+
const nums = tokens.map((t) => {
|
|
188
|
+
if (t in DAY_LOOKUP) return DAY_LOOKUP[t];
|
|
189
|
+
const n = Number(t);
|
|
190
|
+
return Number.isInteger(n) && n >= 0 && n <= 6 ? n : null;
|
|
191
|
+
});
|
|
192
|
+
if (nums.some((n) => n === null)) return null;
|
|
193
|
+
return [...new Set(nums)].sort((a, b) => a - b);
|
|
194
|
+
};
|
|
195
|
+
FREQUENCIES = ["daily", "weekdays", "weekends", "custom"];
|
|
196
|
+
CronBuilderSchema = z3.object({
|
|
197
|
+
hour: z3.number().int().min(0).max(23),
|
|
198
|
+
minute: z3.number().int().min(0).max(59),
|
|
199
|
+
frequency: z3.enum(FREQUENCIES),
|
|
200
|
+
days: z3.array(z3.number().int().min(0).max(6)).optional()
|
|
201
|
+
}).refine((v) => v.frequency !== "custom" || (v.days?.length ?? 0) > 0, {
|
|
202
|
+
message: "custom frequency requires at least one weekday"
|
|
203
|
+
});
|
|
204
|
+
dowField = (frequency, days = []) => {
|
|
205
|
+
switch (frequency) {
|
|
206
|
+
case "daily":
|
|
207
|
+
return "*";
|
|
208
|
+
case "weekdays":
|
|
209
|
+
return "1-5";
|
|
210
|
+
case "weekends":
|
|
211
|
+
return "0,6";
|
|
212
|
+
case "custom":
|
|
213
|
+
return [...new Set(days)].sort((a, b) => a - b).join(",");
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
buildCron = (input) => {
|
|
217
|
+
const { hour, minute, frequency, days } = CronBuilderSchema.parse(input);
|
|
218
|
+
return `0 ${minute} ${hour} * * ${dowField(frequency, days)}`;
|
|
219
|
+
};
|
|
160
220
|
ScheduledJobSchema = z3.object({
|
|
161
221
|
name: z3.string().min(1).regex(/^[a-z0-9-]+$/, "name must be lowercase alphanumeric with dashes"),
|
|
162
222
|
schedule: z3.string().min(1).refine(isValidCron, { message: "schedule must be a 6-field cron expression (e.g. 0 0 9 * * *)" }),
|
|
@@ -242,8 +302,70 @@ var init_schedule = __esm({
|
|
|
242
302
|
}
|
|
243
303
|
});
|
|
244
304
|
|
|
305
|
+
// src/core/compose.ts
|
|
306
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
307
|
+
import { dirname as dirname5 } from "path";
|
|
308
|
+
var composeTemplate, writeCompose;
|
|
309
|
+
var init_compose = __esm({
|
|
310
|
+
"src/core/compose.ts"() {
|
|
311
|
+
"use strict";
|
|
312
|
+
init_paths();
|
|
313
|
+
composeTemplate = (timezone) => `services:
|
|
314
|
+
openwa-api:
|
|
315
|
+
image: ghcr.io/deckasoft/openwa:latest
|
|
316
|
+
ports:
|
|
317
|
+
- '2785:2785'
|
|
318
|
+
environment:
|
|
319
|
+
- NODE_ENV=production
|
|
320
|
+
- PORT=2785
|
|
321
|
+
- DATABASE_TYPE=sqlite
|
|
322
|
+
- DATABASE_SYNCHRONIZE=true
|
|
323
|
+
- ENGINE_TYPE=whatsapp-web.js
|
|
324
|
+
- SESSION_DATA_PATH=/app/data/sessions
|
|
325
|
+
- PUPPETEER_HEADLESS=true
|
|
326
|
+
- PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu
|
|
327
|
+
- STORAGE_TYPE=local
|
|
328
|
+
- STORAGE_LOCAL_PATH=/app/data/media
|
|
329
|
+
- QUEUE_ENABLED=false
|
|
330
|
+
- REDIS_ENABLED=false
|
|
331
|
+
- REDIS_BUILTIN=false
|
|
332
|
+
volumes:
|
|
333
|
+
- openwa-data:/app/data
|
|
334
|
+
networks:
|
|
335
|
+
- waify-network
|
|
336
|
+
restart: unless-stopped
|
|
337
|
+
|
|
338
|
+
scheduler:
|
|
339
|
+
image: mcuadros/ofelia:latest
|
|
340
|
+
depends_on:
|
|
341
|
+
- openwa-api
|
|
342
|
+
command: daemon --config=/etc/ofelia/config.ini
|
|
343
|
+
environment:
|
|
344
|
+
- TZ=${timezone}
|
|
345
|
+
volumes:
|
|
346
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
347
|
+
- ${schedulePath()}:/etc/ofelia/config.ini:ro
|
|
348
|
+
networks:
|
|
349
|
+
- waify-network
|
|
350
|
+
restart: unless-stopped
|
|
351
|
+
|
|
352
|
+
volumes:
|
|
353
|
+
openwa-data:
|
|
354
|
+
|
|
355
|
+
networks:
|
|
356
|
+
waify-network:
|
|
357
|
+
name: waify-network
|
|
358
|
+
`;
|
|
359
|
+
writeCompose = (timezone) => {
|
|
360
|
+
const path = composePath();
|
|
361
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
362
|
+
writeFileSync5(path, composeTemplate(timezone), "utf-8");
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
245
367
|
// src/core/secrets.ts
|
|
246
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as
|
|
368
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
247
369
|
import { z as z4 } from "zod";
|
|
248
370
|
var SecretsSchema, parseEnvFile, loadSecrets, tryLoadSecrets, saveSecrets;
|
|
249
371
|
var init_secrets = __esm({
|
|
@@ -271,7 +393,7 @@ var init_secrets = __esm({
|
|
|
271
393
|
const existing = existsSync5(path) ? parseEnvFile(readFileSync4(path, "utf-8")) : {};
|
|
272
394
|
const merged = { ...existing, ...next };
|
|
273
395
|
const body = Object.entries(merged).filter(([, v]) => typeof v === "string" && v.length > 0).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
274
|
-
|
|
396
|
+
writeFileSync6(path, body + "\n", "utf-8");
|
|
275
397
|
};
|
|
276
398
|
}
|
|
277
399
|
});
|
|
@@ -282,7 +404,7 @@ var composeSystemInstruction, createGeminiProvider;
|
|
|
282
404
|
var init_gemini = __esm({
|
|
283
405
|
"src/core/providers/gemini.ts"() {
|
|
284
406
|
"use strict";
|
|
285
|
-
composeSystemInstruction = (systemPrompt, examples) => {
|
|
407
|
+
composeSystemInstruction = (systemPrompt, examples, language) => {
|
|
286
408
|
const exampleLines = examples.map((e) => `- "${e}"`).join("\n");
|
|
287
409
|
return [
|
|
288
410
|
systemPrompt,
|
|
@@ -290,16 +412,17 @@ var init_gemini = __esm({
|
|
|
290
412
|
"Here are examples of the exact tone and style to match:",
|
|
291
413
|
exampleLines,
|
|
292
414
|
"",
|
|
415
|
+
`Write the message in ${language}, regardless of the language of the examples above.`,
|
|
293
416
|
"Output only the message text \u2014 no quotes, no labels, no explanations."
|
|
294
417
|
].join("\n");
|
|
295
418
|
};
|
|
296
419
|
createGeminiProvider = ({ apiKey, model = "gemini-2.5-flash" }) => ({
|
|
297
|
-
generateMessage: async (systemPrompt, examples) => {
|
|
420
|
+
generateMessage: async ({ systemPrompt, examples, language }) => {
|
|
298
421
|
const ai = new GoogleGenAI({ apiKey });
|
|
299
422
|
const response = await ai.models.generateContent({
|
|
300
423
|
model,
|
|
301
424
|
contents: "Send the message.",
|
|
302
|
-
config: { systemInstruction: composeSystemInstruction(systemPrompt, examples) }
|
|
425
|
+
config: { systemInstruction: composeSystemInstruction(systemPrompt, examples, language) }
|
|
303
426
|
});
|
|
304
427
|
const text = response.text;
|
|
305
428
|
if (!text) {
|
|
@@ -347,8 +470,8 @@ var init_sender = __esm({
|
|
|
347
470
|
});
|
|
348
471
|
|
|
349
472
|
// src/core/logger.ts
|
|
350
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as
|
|
351
|
-
import { dirname as
|
|
473
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
|
|
474
|
+
import { dirname as dirname7 } from "path";
|
|
352
475
|
var log, LINE_RE, parseLine, readHistory;
|
|
353
476
|
var init_logger = __esm({
|
|
354
477
|
"src/core/logger.ts"() {
|
|
@@ -356,7 +479,7 @@ var init_logger = __esm({
|
|
|
356
479
|
init_paths();
|
|
357
480
|
log = (status, detail) => {
|
|
358
481
|
const path = logPath();
|
|
359
|
-
|
|
482
|
+
mkdirSync7(dirname7(path), { recursive: true });
|
|
360
483
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
361
484
|
const line = `[${timestamp}] ${status.toUpperCase()} | ${detail}
|
|
362
485
|
`;
|
|
@@ -380,6 +503,56 @@ var init_logger = __esm({
|
|
|
380
503
|
}
|
|
381
504
|
});
|
|
382
505
|
|
|
506
|
+
// src/core/scheduler.ts
|
|
507
|
+
import { spawn } from "child_process";
|
|
508
|
+
var restartScheduler;
|
|
509
|
+
var init_scheduler = __esm({
|
|
510
|
+
"src/core/scheduler.ts"() {
|
|
511
|
+
"use strict";
|
|
512
|
+
init_paths();
|
|
513
|
+
restartScheduler = () => new Promise((resolve) => {
|
|
514
|
+
const child = spawn("docker", [
|
|
515
|
+
"compose",
|
|
516
|
+
"-f",
|
|
517
|
+
composePath(),
|
|
518
|
+
"up",
|
|
519
|
+
"-d",
|
|
520
|
+
"--force-recreate",
|
|
521
|
+
"scheduler"
|
|
522
|
+
]);
|
|
523
|
+
const chunks = [];
|
|
524
|
+
const collect = (data) => chunks.push(data);
|
|
525
|
+
child.stdout?.on("data", collect);
|
|
526
|
+
child.stderr?.on("data", collect);
|
|
527
|
+
child.on("error", (err) => resolve({ ok: false, output: err.message }));
|
|
528
|
+
child.on(
|
|
529
|
+
"close",
|
|
530
|
+
(code) => resolve({ ok: code === 0, output: Buffer.concat(chunks).toString().trim() })
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// src/core/session.ts
|
|
537
|
+
var stopSession;
|
|
538
|
+
var init_session = __esm({
|
|
539
|
+
"src/core/session.ts"() {
|
|
540
|
+
"use strict";
|
|
541
|
+
stopSession = async ({ baseUrl, apiKey, sessionId }) => {
|
|
542
|
+
if (!sessionId) {
|
|
543
|
+
throw new Error("No active session to disconnect.");
|
|
544
|
+
}
|
|
545
|
+
const response = await fetch(`${baseUrl}/api/sessions/${sessionId}/stop`, {
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: { "X-API-Key": apiKey }
|
|
548
|
+
});
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new Error(`OpenWA responded with ${response.status}: ${await response.text()}`);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
383
556
|
// src/tui/screens/Home.tsx
|
|
384
557
|
import { Box, Text, useInput } from "ink";
|
|
385
558
|
import Spinner from "ink-spinner";
|
|
@@ -394,6 +567,7 @@ var init_Home = __esm({
|
|
|
394
567
|
init_prompt();
|
|
395
568
|
init_gemini();
|
|
396
569
|
init_sender();
|
|
570
|
+
init_session();
|
|
397
571
|
init_logger();
|
|
398
572
|
init_schedule();
|
|
399
573
|
Home = () => {
|
|
@@ -414,7 +588,7 @@ var init_Home = __esm({
|
|
|
414
588
|
try {
|
|
415
589
|
const prompt = loadPrompt();
|
|
416
590
|
const provider = createGeminiProvider({ apiKey: secrets.GEMINI_API_KEY });
|
|
417
|
-
const text = await generateMessage({ provider, prompt });
|
|
591
|
+
const text = await generateMessage({ provider, prompt, language: cfg.language });
|
|
418
592
|
setState({ kind: "preview", text });
|
|
419
593
|
} catch (err) {
|
|
420
594
|
setState({ kind: "error", message: err instanceof Error ? err.message : String(err) });
|
|
@@ -435,7 +609,7 @@ var init_Home = __esm({
|
|
|
435
609
|
try {
|
|
436
610
|
const prompt = loadPrompt();
|
|
437
611
|
const provider = createGeminiProvider({ apiKey: secrets.GEMINI_API_KEY ?? "" });
|
|
438
|
-
const text = await generateMessage({ provider, prompt });
|
|
612
|
+
const text = await generateMessage({ provider, prompt, language: cfg.language });
|
|
439
613
|
await sendMessage({
|
|
440
614
|
baseUrl: cfg.openwaBaseUrl,
|
|
441
615
|
apiKey: secrets.OPENWA_API_KEY ?? "",
|
|
@@ -451,10 +625,30 @@ var init_Home = __esm({
|
|
|
451
625
|
setState({ kind: "error", message });
|
|
452
626
|
}
|
|
453
627
|
};
|
|
628
|
+
const doDisconnect = async () => {
|
|
629
|
+
setState({ kind: "busy", label: "Disconnecting WhatsApp" });
|
|
630
|
+
try {
|
|
631
|
+
await stopSession({
|
|
632
|
+
baseUrl: cfg.openwaBaseUrl,
|
|
633
|
+
apiKey: secrets.OPENWA_API_KEY ?? "",
|
|
634
|
+
sessionId: cfg.openwaSessionId ?? ""
|
|
635
|
+
});
|
|
636
|
+
saveConfig({ ...cfg, openwaSessionId: null });
|
|
637
|
+
setState({ kind: "disconnected" });
|
|
638
|
+
} catch (err) {
|
|
639
|
+
setState({ kind: "error", message: err instanceof Error ? err.message : String(err) });
|
|
640
|
+
}
|
|
641
|
+
};
|
|
454
642
|
useInput((input) => {
|
|
455
643
|
if (state.kind === "busy") return;
|
|
644
|
+
if (state.kind === "confirm-disconnect") {
|
|
645
|
+
if (input === "y") void doDisconnect();
|
|
646
|
+
else setState({ kind: "idle" });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
456
649
|
if (input === "p") void doPreview();
|
|
457
650
|
if (input === "s") void doSend();
|
|
651
|
+
if (input === "x") setState({ kind: "confirm-disconnect" });
|
|
458
652
|
});
|
|
459
653
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
|
|
460
654
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
@@ -478,7 +672,16 @@ var init_Home = __esm({
|
|
|
478
672
|
] }),
|
|
479
673
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
480
674
|
/* @__PURE__ */ jsx(Text, { bold: true, children: "Actions" }),
|
|
481
|
-
/* @__PURE__ */ jsx(Text, { children: " [p] preview \xB7 [s] send now" })
|
|
675
|
+
/* @__PURE__ */ jsx(Text, { children: " [p] preview \xB7 [s] send now \xB7 [x] disconnect WhatsApp" })
|
|
676
|
+
] }),
|
|
677
|
+
state.kind === "confirm-disconnect" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
678
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "Disconnect WhatsApp?" }),
|
|
679
|
+
/* @__PURE__ */ jsx(Text, { children: "This stops the session. Reconnecting means re-running `waify setup` (scan the QR)." }),
|
|
680
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[y] confirm \xB7 any other key cancels" })
|
|
681
|
+
] }),
|
|
682
|
+
state.kind === "disconnected" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [
|
|
683
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "WhatsApp disconnected" }),
|
|
684
|
+
/* @__PURE__ */ jsx(Text, { children: "Run `waify setup` to reconnect." })
|
|
482
685
|
] }),
|
|
483
686
|
state.kind === "busy" && /* @__PURE__ */ jsxs(Text, { children: [
|
|
484
687
|
/* @__PURE__ */ jsx(Spinner, { type: "dots" }),
|
|
@@ -550,42 +753,106 @@ var init_History = __esm({
|
|
|
550
753
|
}
|
|
551
754
|
});
|
|
552
755
|
|
|
553
|
-
// src/tui/
|
|
756
|
+
// src/tui/components/SelectList.tsx
|
|
554
757
|
import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
|
|
555
758
|
import TextInput from "ink-text-input";
|
|
556
|
-
import {
|
|
759
|
+
import { useState as useState3 } from "react";
|
|
557
760
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
558
|
-
var
|
|
761
|
+
var SelectList;
|
|
762
|
+
var init_SelectList = __esm({
|
|
763
|
+
"src/tui/components/SelectList.tsx"() {
|
|
764
|
+
"use strict";
|
|
765
|
+
SelectList = ({ items, onSelect, onCancel, filterable = false, pageSize = 8 }) => {
|
|
766
|
+
const [filter, setFilter] = useState3("");
|
|
767
|
+
const [cursor, setCursor] = useState3(0);
|
|
768
|
+
const filtered = filter ? items.filter((i) => i.toLowerCase().includes(filter.toLowerCase())) : items;
|
|
769
|
+
const active = Math.min(cursor, Math.max(0, filtered.length - 1));
|
|
770
|
+
useInput3((_input, key) => {
|
|
771
|
+
if (key.escape) return onCancel();
|
|
772
|
+
if (key.upArrow) return setCursor((c) => Math.max(0, c - 1));
|
|
773
|
+
if (key.downArrow) return setCursor((c) => Math.min(filtered.length - 1, c + 1));
|
|
774
|
+
if (key.return) {
|
|
775
|
+
const item = filtered[active];
|
|
776
|
+
if (item) onSelect(item);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
const start = Math.max(
|
|
780
|
+
0,
|
|
781
|
+
Math.min(active - Math.floor(pageSize / 2), Math.max(0, filtered.length - pageSize))
|
|
782
|
+
);
|
|
783
|
+
const view = filtered.slice(start, start + pageSize);
|
|
784
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
785
|
+
filterable && /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
786
|
+
/* @__PURE__ */ jsx3(Text3, { children: "filter: " }),
|
|
787
|
+
/* @__PURE__ */ jsx3(
|
|
788
|
+
TextInput,
|
|
789
|
+
{
|
|
790
|
+
value: filter,
|
|
791
|
+
onChange: (v) => {
|
|
792
|
+
setFilter(v);
|
|
793
|
+
setCursor(0);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
)
|
|
797
|
+
] }),
|
|
798
|
+
view.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "(no matches)" }) : view.map((item, i) => {
|
|
799
|
+
const idx = start + i;
|
|
800
|
+
return /* @__PURE__ */ jsxs3(Text3, { color: idx === active ? "cyan" : void 0, children: [
|
|
801
|
+
idx === active ? "\u25B8 " : " ",
|
|
802
|
+
item
|
|
803
|
+
] }, item);
|
|
804
|
+
}),
|
|
805
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
806
|
+
"\u2191/\u2193 move \xB7 enter select \xB7 esc cancel",
|
|
807
|
+
filterable ? " \xB7 type to filter" : ""
|
|
808
|
+
] })
|
|
809
|
+
] });
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// src/tui/screens/Settings.tsx
|
|
815
|
+
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
816
|
+
import TextInput2 from "ink-text-input";
|
|
817
|
+
import { useEffect, useState as useState4 } from "react";
|
|
818
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
819
|
+
var buildFields, LANGUAGE_OPTIONS, Settings;
|
|
559
820
|
var init_Settings = __esm({
|
|
560
821
|
"src/tui/screens/Settings.tsx"() {
|
|
561
822
|
"use strict";
|
|
562
823
|
init_config();
|
|
563
824
|
init_secrets();
|
|
825
|
+
init_compose();
|
|
826
|
+
init_scheduler();
|
|
827
|
+
init_SelectList();
|
|
564
828
|
buildFields = () => {
|
|
565
829
|
const cfg = loadConfig();
|
|
566
830
|
const secrets = tryLoadSecrets();
|
|
567
831
|
return [
|
|
568
|
-
{ key: "openwaBaseUrl", label: "openwaBaseUrl", group: "config", value: cfg.openwaBaseUrl, secret: false },
|
|
569
|
-
{ key: "openwaSessionId", label: "openwaSessionId", group: "config", value: cfg.openwaSessionId ?? "", secret: false },
|
|
570
|
-
{ key: "openwaApiKey", label: "openwaApiKey", group: "config", value: cfg.openwaApiKey ?? "", secret: false },
|
|
571
|
-
{ key: "recipientChatId", label: "recipients[0].chatId", group: "recipient", value: cfg.recipients[0]?.chatId ?? "", secret: false },
|
|
572
|
-
{ key: "
|
|
573
|
-
{ key: "
|
|
832
|
+
{ key: "openwaBaseUrl", label: "openwaBaseUrl", group: "config", kind: "text", value: cfg.openwaBaseUrl, secret: false },
|
|
833
|
+
{ key: "openwaSessionId", label: "openwaSessionId", group: "config", kind: "text", value: cfg.openwaSessionId ?? "", secret: false },
|
|
834
|
+
{ key: "openwaApiKey", label: "openwaApiKey", group: "config", kind: "text", value: cfg.openwaApiKey ?? "", secret: false },
|
|
835
|
+
{ key: "recipientChatId", label: "recipients[0].chatId", group: "recipient", kind: "text", value: cfg.recipients[0]?.chatId ?? "", secret: false },
|
|
836
|
+
{ key: "language", label: "language", group: "config", kind: "language", value: cfg.language, secret: false },
|
|
837
|
+
{ key: "timezone", label: "timezone", group: "config", kind: "timezone", value: cfg.timezone, secret: false },
|
|
838
|
+
{ key: "GEMINI_API_KEY", label: "GEMINI_API_KEY", group: "secret", kind: "text", value: secrets.GEMINI_API_KEY ?? "", secret: true },
|
|
839
|
+
{ key: "OPENWA_API_KEY", label: "OPENWA_API_KEY", group: "secret", kind: "text", value: secrets.OPENWA_API_KEY ?? "", secret: true }
|
|
574
840
|
];
|
|
575
841
|
};
|
|
842
|
+
LANGUAGE_OPTIONS = [...LANGUAGES, "Other\u2026"];
|
|
576
843
|
Settings = ({ onFocusChange }) => {
|
|
577
|
-
const [fields, setFields] =
|
|
578
|
-
const [cursor, setCursor] =
|
|
579
|
-
const [
|
|
580
|
-
const [draft, setDraft] =
|
|
581
|
-
const [message, setMessage] =
|
|
844
|
+
const [fields, setFields] = useState4(buildFields);
|
|
845
|
+
const [cursor, setCursor] = useState4(0);
|
|
846
|
+
const [mode, setMode] = useState4("none");
|
|
847
|
+
const [draft, setDraft] = useState4("");
|
|
848
|
+
const [message, setMessage] = useState4(null);
|
|
582
849
|
useEffect(() => {
|
|
583
|
-
onFocusChange(
|
|
584
|
-
}, [
|
|
585
|
-
|
|
586
|
-
if (
|
|
587
|
-
if (key.escape) {
|
|
588
|
-
|
|
850
|
+
onFocusChange(mode !== "none");
|
|
851
|
+
}, [mode, onFocusChange]);
|
|
852
|
+
useInput4((input, key) => {
|
|
853
|
+
if (mode !== "none") {
|
|
854
|
+
if ((mode === "text" || mode === "language-other") && key.escape) {
|
|
855
|
+
setMode("none");
|
|
589
856
|
setDraft("");
|
|
590
857
|
}
|
|
591
858
|
return;
|
|
@@ -594,85 +861,143 @@ var init_Settings = __esm({
|
|
|
594
861
|
if (key.downArrow || input === "j") setCursor((c) => Math.min(fields.length - 1, c + 1));
|
|
595
862
|
if (key.return || input === "e") {
|
|
596
863
|
const field = fields[cursor];
|
|
597
|
-
if (field)
|
|
864
|
+
if (!field) return;
|
|
865
|
+
if (field.kind === "language") setMode("language");
|
|
866
|
+
else if (field.kind === "timezone") setMode("timezone");
|
|
867
|
+
else {
|
|
598
868
|
setDraft(field.value);
|
|
599
|
-
|
|
869
|
+
setMode("text");
|
|
600
870
|
}
|
|
601
871
|
}
|
|
602
872
|
});
|
|
603
|
-
const
|
|
873
|
+
const saveConfigField = (key, value) => {
|
|
874
|
+
saveConfig(ConfigSchema.parse({ ...loadConfig(), [key]: value }));
|
|
875
|
+
};
|
|
876
|
+
const finish = (msg) => {
|
|
877
|
+
setFields(buildFields());
|
|
878
|
+
setMessage(msg);
|
|
879
|
+
setMode("none");
|
|
880
|
+
setDraft("");
|
|
881
|
+
};
|
|
882
|
+
const commitText = (value) => {
|
|
604
883
|
const field = fields[cursor];
|
|
605
884
|
if (!field) return;
|
|
606
885
|
try {
|
|
607
886
|
if (field.group === "secret") {
|
|
608
|
-
|
|
609
|
-
saveSecrets(parsed);
|
|
887
|
+
saveSecrets(SecretsSchema.partial().parse({ [field.key]: value }));
|
|
610
888
|
} else if (field.group === "recipient") {
|
|
611
889
|
const cfg = loadConfig();
|
|
612
|
-
|
|
613
|
-
const next = ConfigSchema.parse({
|
|
614
|
-
...cfg,
|
|
615
|
-
recipients: [{ ...existing, chatId: value }]
|
|
616
|
-
});
|
|
617
|
-
saveConfig(next);
|
|
890
|
+
saveConfig(ConfigSchema.parse({ ...cfg, recipients: [{ ...cfg.recipients[0], chatId: value }] }));
|
|
618
891
|
} else {
|
|
619
|
-
|
|
620
|
-
const next = ConfigSchema.parse({ ...cfg, [field.key]: value || null });
|
|
621
|
-
saveConfig(next);
|
|
892
|
+
saveConfigField(field.key, value || null);
|
|
622
893
|
}
|
|
894
|
+
finish(`saved ${field.label}`);
|
|
895
|
+
} catch (err) {
|
|
896
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
897
|
+
setMode("none");
|
|
898
|
+
setDraft("");
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
const commitLanguage = (value) => {
|
|
902
|
+
if (value === "Other\u2026") {
|
|
903
|
+
setDraft("");
|
|
904
|
+
setMode("language-other");
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const lang = value.trim();
|
|
908
|
+
if (!lang) {
|
|
909
|
+
setMode("none");
|
|
910
|
+
setDraft("");
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
saveConfigField("language", lang);
|
|
915
|
+
finish(`saved language \u2192 ${lang}`);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
918
|
+
setMode("none");
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
const commitTimezone = async (value) => {
|
|
922
|
+
setMode("none");
|
|
923
|
+
try {
|
|
924
|
+
saveConfigField("timezone", value);
|
|
623
925
|
setFields(buildFields());
|
|
624
|
-
|
|
926
|
+
writeCompose(value);
|
|
927
|
+
setMessage(`timezone \u2192 ${value} \xB7 restarting scheduler\u2026`);
|
|
928
|
+
const res = await restartScheduler();
|
|
929
|
+
setMessage(
|
|
930
|
+
res.ok ? `timezone \u2192 ${value} \xB7 scheduler restarted` : `timezone saved \xB7 restart failed \u2014 run: docker compose up -d --force-recreate scheduler`
|
|
931
|
+
);
|
|
625
932
|
} catch (err) {
|
|
626
933
|
setMessage(err instanceof Error ? err.message : String(err));
|
|
627
934
|
}
|
|
628
|
-
setEditing(false);
|
|
629
|
-
setDraft("");
|
|
630
935
|
};
|
|
936
|
+
if (mode === "language") {
|
|
937
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
938
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "Settings \xB7 language" }),
|
|
939
|
+
/* @__PURE__ */ jsx4(SelectList, { items: LANGUAGE_OPTIONS, onSelect: commitLanguage, onCancel: () => setMode("none") })
|
|
940
|
+
] });
|
|
941
|
+
}
|
|
942
|
+
if (mode === "timezone") {
|
|
943
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
944
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "Settings \xB7 timezone" }),
|
|
945
|
+
/* @__PURE__ */ jsx4(
|
|
946
|
+
SelectList,
|
|
947
|
+
{
|
|
948
|
+
items: supportedTimezones(),
|
|
949
|
+
filterable: true,
|
|
950
|
+
onSelect: (v) => void commitTimezone(v),
|
|
951
|
+
onCancel: () => setMode("none")
|
|
952
|
+
}
|
|
953
|
+
)
|
|
954
|
+
] });
|
|
955
|
+
}
|
|
631
956
|
const renderValue = (f) => {
|
|
632
|
-
if (!f.value) return /* @__PURE__ */
|
|
633
|
-
if (f.secret) return /* @__PURE__ */
|
|
957
|
+
if (!f.value) return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(unset)" });
|
|
958
|
+
if (f.secret) return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
634
959
|
f.value.slice(0, 4),
|
|
635
960
|
"\u2026"
|
|
636
961
|
] });
|
|
637
|
-
return /* @__PURE__ */
|
|
962
|
+
return /* @__PURE__ */ jsx4(Text4, { children: f.value });
|
|
638
963
|
};
|
|
639
|
-
return /* @__PURE__ */
|
|
640
|
-
/* @__PURE__ */
|
|
641
|
-
/* @__PURE__ */
|
|
642
|
-
/* @__PURE__ */
|
|
643
|
-
/* @__PURE__ */
|
|
964
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
965
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, children: "Settings" }),
|
|
966
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191/\u2193 to move \xB7 enter to edit \xB7 esc to cancel" }),
|
|
967
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: fields.map((f, i) => /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
968
|
+
/* @__PURE__ */ jsx4(Box4, { width: 24, children: /* @__PURE__ */ jsxs4(Text4, { color: i === cursor ? "cyan" : void 0, children: [
|
|
644
969
|
i === cursor ? "\u25B8 " : " ",
|
|
645
970
|
f.label
|
|
646
971
|
] }) }),
|
|
647
|
-
/* @__PURE__ */
|
|
648
|
-
|
|
972
|
+
/* @__PURE__ */ jsx4(Text4, { children: " = " }),
|
|
973
|
+
mode === "text" && i === cursor ? /* @__PURE__ */ jsx4(TextInput2, { value: draft, onChange: setDraft, onSubmit: commitText }) : mode === "language-other" && i === cursor ? /* @__PURE__ */ jsx4(TextInput2, { value: draft, onChange: setDraft, onSubmit: commitLanguage, placeholder: "language name" }) : renderValue(f)
|
|
649
974
|
] }, f.key)) }),
|
|
650
|
-
message && /* @__PURE__ */
|
|
975
|
+
message && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "green", children: message }) })
|
|
651
976
|
] });
|
|
652
977
|
};
|
|
653
978
|
}
|
|
654
979
|
});
|
|
655
980
|
|
|
656
981
|
// src/tui/screens/Prompt.tsx
|
|
657
|
-
import { Box as
|
|
658
|
-
import
|
|
659
|
-
import { useEffect as useEffect2, useState as
|
|
660
|
-
import { jsx as
|
|
982
|
+
import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
|
|
983
|
+
import TextInput3 from "ink-text-input";
|
|
984
|
+
import { useEffect as useEffect2, useState as useState5 } from "react";
|
|
985
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
661
986
|
var PromptScreen;
|
|
662
987
|
var init_Prompt = __esm({
|
|
663
988
|
"src/tui/screens/Prompt.tsx"() {
|
|
664
989
|
"use strict";
|
|
665
990
|
init_prompt();
|
|
666
991
|
PromptScreen = ({ onFocusChange }) => {
|
|
667
|
-
const [prompt, setPrompt] =
|
|
668
|
-
const [cursor, setCursor] =
|
|
669
|
-
const [adding, setAdding] =
|
|
670
|
-
const [draft, setDraft] =
|
|
671
|
-
const [message, setMessage] =
|
|
992
|
+
const [prompt, setPrompt] = useState5(loadPrompt);
|
|
993
|
+
const [cursor, setCursor] = useState5(0);
|
|
994
|
+
const [adding, setAdding] = useState5(false);
|
|
995
|
+
const [draft, setDraft] = useState5("");
|
|
996
|
+
const [message, setMessage] = useState5(null);
|
|
672
997
|
useEffect2(() => {
|
|
673
998
|
onFocusChange(adding);
|
|
674
999
|
}, [adding, onFocusChange]);
|
|
675
|
-
|
|
1000
|
+
useInput5((input, key) => {
|
|
676
1001
|
if (adding) {
|
|
677
1002
|
if (key.escape) {
|
|
678
1003
|
setAdding(false);
|
|
@@ -711,68 +1036,141 @@ var init_Prompt = __esm({
|
|
|
711
1036
|
setDraft("");
|
|
712
1037
|
setMessage(`added example #${next.examples.length}`);
|
|
713
1038
|
};
|
|
714
|
-
return /* @__PURE__ */
|
|
715
|
-
/* @__PURE__ */
|
|
716
|
-
/* @__PURE__ */
|
|
717
|
-
/* @__PURE__ */
|
|
718
|
-
/* @__PURE__ */
|
|
719
|
-
/* @__PURE__ */
|
|
1039
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1040
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Prompt" }),
|
|
1041
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "system prompt is shown read-only here; edit via `wife prompt edit`" }),
|
|
1042
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, borderStyle: "single", paddingX: 1, flexDirection: "column", children: prompt.systemPrompt.split("\n").map((line, i) => /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: line || " " }, i)) }),
|
|
1043
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1044
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
720
1045
|
"Examples (",
|
|
721
1046
|
prompt.examples.length,
|
|
722
1047
|
")"
|
|
723
1048
|
] }),
|
|
724
|
-
/* @__PURE__ */
|
|
725
|
-
prompt.examples.map((e, i) => /* @__PURE__ */
|
|
1049
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191/\u2193 to move \xB7 [a]dd \xB7 [d]elete" }),
|
|
1050
|
+
prompt.examples.map((e, i) => /* @__PURE__ */ jsxs5(Text5, { color: i === cursor ? "cyan" : void 0, children: [
|
|
726
1051
|
i === cursor ? "\u25B8 " : " ",
|
|
727
1052
|
i + 1,
|
|
728
1053
|
". ",
|
|
729
1054
|
e
|
|
730
1055
|
] }, i)),
|
|
731
|
-
adding && /* @__PURE__ */
|
|
732
|
-
/* @__PURE__ */
|
|
733
|
-
/* @__PURE__ */
|
|
1056
|
+
adding && /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1057
|
+
/* @__PURE__ */ jsx5(Text5, { children: "+ " }),
|
|
1058
|
+
/* @__PURE__ */ jsx5(TextInput3, { value: draft, onChange: setDraft, onSubmit: commit })
|
|
734
1059
|
] })
|
|
735
1060
|
] }),
|
|
736
|
-
message && /* @__PURE__ */
|
|
1061
|
+
message && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "green", children: message }) })
|
|
1062
|
+
] });
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// src/tui/components/DayPicker.tsx
|
|
1068
|
+
import { Box as Box6, Text as Text6, useInput as useInput6 } from "ink";
|
|
1069
|
+
import { useState as useState6 } from "react";
|
|
1070
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1071
|
+
var DayPicker;
|
|
1072
|
+
var init_DayPicker = __esm({
|
|
1073
|
+
"src/tui/components/DayPicker.tsx"() {
|
|
1074
|
+
"use strict";
|
|
1075
|
+
init_schedule();
|
|
1076
|
+
DayPicker = ({ onSubmit, onCancel }) => {
|
|
1077
|
+
const [cursor, setCursor] = useState6(1);
|
|
1078
|
+
const [selected, setSelected] = useState6(/* @__PURE__ */ new Set());
|
|
1079
|
+
useInput6((input, key) => {
|
|
1080
|
+
if (key.escape) return onCancel();
|
|
1081
|
+
if (key.leftArrow) return setCursor((c) => Math.max(0, c - 1));
|
|
1082
|
+
if (key.rightArrow) return setCursor((c) => Math.min(DAY_LABELS.length - 1, c + 1));
|
|
1083
|
+
if (input === " ") {
|
|
1084
|
+
return setSelected((s) => {
|
|
1085
|
+
const next = new Set(s);
|
|
1086
|
+
if (next.has(cursor)) next.delete(cursor);
|
|
1087
|
+
else next.add(cursor);
|
|
1088
|
+
return next;
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
if (key.return && selected.size > 0) {
|
|
1092
|
+
onSubmit([...selected].sort((a, b) => a - b));
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1096
|
+
/* @__PURE__ */ jsx6(Box6, { children: DAY_LABELS.map((d, i) => /* @__PURE__ */ jsxs6(Text6, { color: i === cursor ? "cyan" : selected.has(i) ? "green" : void 0, children: [
|
|
1097
|
+
selected.has(i) ? `[${d}]` : ` ${d} `,
|
|
1098
|
+
" "
|
|
1099
|
+
] }, d)) }),
|
|
1100
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2190/\u2192 move \xB7 space toggle \xB7 enter confirm \xB7 esc cancel" })
|
|
737
1101
|
] });
|
|
738
1102
|
};
|
|
739
1103
|
}
|
|
740
1104
|
});
|
|
741
1105
|
|
|
742
1106
|
// src/tui/screens/Schedule.tsx
|
|
743
|
-
import { Box as
|
|
744
|
-
import
|
|
745
|
-
import { useEffect as useEffect3, useState as
|
|
746
|
-
import { jsx as
|
|
747
|
-
var ScheduleScreen;
|
|
1107
|
+
import { Box as Box7, Text as Text7, useInput as useInput7 } from "ink";
|
|
1108
|
+
import TextInput4 from "ink-text-input";
|
|
1109
|
+
import { useEffect as useEffect3, useState as useState7 } from "react";
|
|
1110
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1111
|
+
var HHMM_RE2, FREQUENCY_BY_LABEL, FREQUENCY_LABELS, ScheduleScreen;
|
|
748
1112
|
var init_Schedule = __esm({
|
|
749
1113
|
"src/tui/screens/Schedule.tsx"() {
|
|
750
1114
|
"use strict";
|
|
751
1115
|
init_schedule();
|
|
1116
|
+
init_scheduler();
|
|
1117
|
+
init_SelectList();
|
|
1118
|
+
init_DayPicker();
|
|
1119
|
+
HHMM_RE2 = /^([01]?\d|2[0-3]):([0-5]\d)$/;
|
|
1120
|
+
FREQUENCY_BY_LABEL = {
|
|
1121
|
+
Daily: "daily",
|
|
1122
|
+
Weekdays: "weekdays",
|
|
1123
|
+
Weekends: "weekends",
|
|
1124
|
+
"Custom days": "custom"
|
|
1125
|
+
};
|
|
1126
|
+
FREQUENCY_LABELS = Object.keys(FREQUENCY_BY_LABEL);
|
|
752
1127
|
ScheduleScreen = ({ onFocusChange }) => {
|
|
753
|
-
const [schedule, setSchedule] =
|
|
754
|
-
const [cursor, setCursor] =
|
|
755
|
-
const [addStep, setAddStep] =
|
|
756
|
-
const [draftName, setDraftName] =
|
|
757
|
-
const [
|
|
758
|
-
const [message, setMessage] =
|
|
1128
|
+
const [schedule, setSchedule] = useState7(loadSchedule);
|
|
1129
|
+
const [cursor, setCursor] = useState7(0);
|
|
1130
|
+
const [addStep, setAddStep] = useState7(null);
|
|
1131
|
+
const [draftName, setDraftName] = useState7("");
|
|
1132
|
+
const [draftTime, setDraftTime] = useState7("");
|
|
1133
|
+
const [message, setMessage] = useState7(null);
|
|
759
1134
|
useEffect3(() => {
|
|
760
1135
|
onFocusChange(addStep !== null);
|
|
761
1136
|
}, [addStep, onFocusChange]);
|
|
762
|
-
|
|
1137
|
+
const resetAdd = () => {
|
|
1138
|
+
setAddStep(null);
|
|
1139
|
+
setDraftName("");
|
|
1140
|
+
setDraftTime("");
|
|
1141
|
+
};
|
|
1142
|
+
const applyRestart = async (note) => {
|
|
1143
|
+
setMessage(`${note} \xB7 restarting scheduler\u2026`);
|
|
1144
|
+
const res = await restartScheduler();
|
|
1145
|
+
setMessage(
|
|
1146
|
+
res.ok ? `${note} \xB7 scheduler restarted` : `${note} \xB7 restart failed \u2014 run: docker compose up -d --force-recreate scheduler`
|
|
1147
|
+
);
|
|
1148
|
+
};
|
|
1149
|
+
const finishAdd = async (cron) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const job = ScheduledJobSchema.parse({ name: draftName, schedule: cron, command: "send" });
|
|
1152
|
+
const next = addJob(job);
|
|
1153
|
+
setSchedule(next);
|
|
1154
|
+
resetAdd();
|
|
1155
|
+
await applyRestart(`added "${job.name}"`);
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
1158
|
+
resetAdd();
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
const [hourStr, minuteStr] = draftTime.split(":");
|
|
1162
|
+
const hour = Number(hourStr);
|
|
1163
|
+
const minute = Number(minuteStr);
|
|
1164
|
+
useInput7((input, key) => {
|
|
763
1165
|
if (addStep !== null) {
|
|
764
|
-
if (key.escape)
|
|
765
|
-
setAddStep(null);
|
|
766
|
-
setDraftName("");
|
|
767
|
-
setDraftCron("");
|
|
768
|
-
}
|
|
1166
|
+
if ((addStep === "name" || addStep === "time") && key.escape) resetAdd();
|
|
769
1167
|
return;
|
|
770
1168
|
}
|
|
771
1169
|
if (key.upArrow || input === "k") setCursor((c) => Math.max(0, c - 1));
|
|
772
1170
|
if (key.downArrow || input === "j") setCursor((c) => Math.min(schedule.jobs.length - 1, c + 1));
|
|
773
1171
|
if (input === "a") {
|
|
774
1172
|
setDraftName("");
|
|
775
|
-
|
|
1173
|
+
setDraftTime("");
|
|
776
1174
|
setAddStep("name");
|
|
777
1175
|
}
|
|
778
1176
|
if (input === "d") {
|
|
@@ -782,43 +1180,77 @@ var init_Schedule = __esm({
|
|
|
782
1180
|
const next = removeJob(job.name);
|
|
783
1181
|
setSchedule(next);
|
|
784
1182
|
setCursor((c) => Math.max(0, Math.min(next.jobs.length - 1, c)));
|
|
785
|
-
|
|
1183
|
+
void applyRestart(`removed "${job.name}"`);
|
|
786
1184
|
} catch (err) {
|
|
787
1185
|
setMessage(err instanceof Error ? err.message : String(err));
|
|
788
1186
|
}
|
|
789
1187
|
}
|
|
1188
|
+
if (input === "r") void applyRestart("manual restart");
|
|
790
1189
|
});
|
|
791
1190
|
const submitName = (value) => {
|
|
792
|
-
|
|
793
|
-
|
|
1191
|
+
const name = value.trim();
|
|
1192
|
+
if (name.length === 0) return resetAdd();
|
|
1193
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
1194
|
+
setMessage("Name must be lowercase letters, numbers, and dashes only.");
|
|
794
1195
|
return;
|
|
795
1196
|
}
|
|
796
|
-
setDraftName(
|
|
797
|
-
setAddStep("
|
|
1197
|
+
setDraftName(name);
|
|
1198
|
+
setAddStep("time");
|
|
798
1199
|
};
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1200
|
+
const submitTime = (value) => {
|
|
1201
|
+
const time = value.trim();
|
|
1202
|
+
if (time.length === 0) return resetAdd();
|
|
1203
|
+
if (!HHMM_RE2.test(time)) {
|
|
1204
|
+
setMessage("Invalid time. Use 24h HH:MM, e.g. 09:00.");
|
|
803
1205
|
return;
|
|
804
1206
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1207
|
+
setDraftTime(time);
|
|
1208
|
+
setAddStep("frequency");
|
|
1209
|
+
};
|
|
1210
|
+
const onFrequency = (label) => {
|
|
1211
|
+
const frequency = FREQUENCY_BY_LABEL[label];
|
|
1212
|
+
if (!frequency) return;
|
|
1213
|
+
if (frequency === "custom") {
|
|
1214
|
+
setAddStep("days");
|
|
1215
|
+
return;
|
|
812
1216
|
}
|
|
813
|
-
|
|
814
|
-
setDraftName("");
|
|
815
|
-
setDraftCron("");
|
|
1217
|
+
void finishAdd(buildCron({ hour, minute, frequency }));
|
|
816
1218
|
};
|
|
817
|
-
|
|
818
|
-
/* @__PURE__ */
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1219
|
+
if (addStep === "frequency") {
|
|
1220
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1221
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
|
|
1222
|
+
"Schedule \xB7 ",
|
|
1223
|
+
draftName,
|
|
1224
|
+
" @ ",
|
|
1225
|
+
draftTime,
|
|
1226
|
+
" \u2014 frequency"
|
|
1227
|
+
] }),
|
|
1228
|
+
/* @__PURE__ */ jsx7(SelectList, { items: FREQUENCY_LABELS, onSelect: onFrequency, onCancel: resetAdd })
|
|
1229
|
+
] });
|
|
1230
|
+
}
|
|
1231
|
+
if (addStep === "days") {
|
|
1232
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1233
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
|
|
1234
|
+
"Schedule \xB7 ",
|
|
1235
|
+
draftName,
|
|
1236
|
+
" @ ",
|
|
1237
|
+
draftTime,
|
|
1238
|
+
" \u2014 pick days"
|
|
1239
|
+
] }),
|
|
1240
|
+
/* @__PURE__ */ jsx7(
|
|
1241
|
+
DayPicker,
|
|
1242
|
+
{
|
|
1243
|
+
onSubmit: (days) => void finishAdd(buildCron({ hour, minute, frequency: "custom", days })),
|
|
1244
|
+
onCancel: resetAdd
|
|
1245
|
+
}
|
|
1246
|
+
)
|
|
1247
|
+
] });
|
|
1248
|
+
}
|
|
1249
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1250
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: "Schedule" }),
|
|
1251
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191/\u2193 move \xB7 [a]dd \xB7 [d]elete \xB7 [r] restart scheduler \xB7 changes auto-restart" }),
|
|
1252
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
1253
|
+
schedule.jobs.length === 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(no jobs)" }) : schedule.jobs.map((j, i) => /* @__PURE__ */ jsxs7(Text7, { color: i === cursor ? "cyan" : void 0, children: [
|
|
822
1254
|
i === cursor ? "\u25B8 " : " ",
|
|
823
1255
|
j.name.padEnd(20),
|
|
824
1256
|
" ",
|
|
@@ -826,29 +1258,29 @@ var init_Schedule = __esm({
|
|
|
826
1258
|
" \u2192 ",
|
|
827
1259
|
j.command
|
|
828
1260
|
] }, j.name)),
|
|
829
|
-
addStep === "name" && /* @__PURE__ */
|
|
830
|
-
/* @__PURE__ */
|
|
831
|
-
/* @__PURE__ */
|
|
1261
|
+
addStep === "name" && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
1262
|
+
/* @__PURE__ */ jsx7(Text7, { children: "+ name: " }),
|
|
1263
|
+
/* @__PURE__ */ jsx7(TextInput4, { value: draftName, onChange: setDraftName, onSubmit: submitName })
|
|
832
1264
|
] }),
|
|
833
|
-
addStep === "
|
|
834
|
-
/* @__PURE__ */
|
|
1265
|
+
addStep === "time" && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
1266
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
835
1267
|
"+ ",
|
|
836
1268
|
draftName,
|
|
837
|
-
"
|
|
1269
|
+
" time (HH:MM): "
|
|
838
1270
|
] }),
|
|
839
|
-
/* @__PURE__ */
|
|
1271
|
+
/* @__PURE__ */ jsx7(TextInput4, { value: draftTime, onChange: setDraftTime, onSubmit: submitTime, placeholder: "09:00" })
|
|
840
1272
|
] })
|
|
841
1273
|
] }),
|
|
842
|
-
message && /* @__PURE__ */
|
|
1274
|
+
message && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: message }) })
|
|
843
1275
|
] });
|
|
844
1276
|
};
|
|
845
1277
|
}
|
|
846
1278
|
});
|
|
847
1279
|
|
|
848
1280
|
// src/tui/App.tsx
|
|
849
|
-
import { Box as
|
|
850
|
-
import { useState as
|
|
851
|
-
import { jsx as
|
|
1281
|
+
import { Box as Box8, Text as Text8, useApp, useInput as useInput8 } from "ink";
|
|
1282
|
+
import { useState as useState8 } from "react";
|
|
1283
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
852
1284
|
var TABS, TabBar, App;
|
|
853
1285
|
var init_App = __esm({
|
|
854
1286
|
"src/tui/App.tsx"() {
|
|
@@ -865,17 +1297,17 @@ var init_App = __esm({
|
|
|
865
1297
|
{ key: "4", label: "Prompt" },
|
|
866
1298
|
{ key: "5", label: "Schedule" }
|
|
867
1299
|
];
|
|
868
|
-
TabBar = ({ active }) => /* @__PURE__ */
|
|
1300
|
+
TabBar = ({ active }) => /* @__PURE__ */ jsx8(Box8, { gap: 2, children: TABS.map((t) => /* @__PURE__ */ jsxs8(Text8, { bold: t.key === active, color: t.key === active ? "cyan" : void 0, children: [
|
|
869
1301
|
"[",
|
|
870
1302
|
t.key,
|
|
871
1303
|
"] ",
|
|
872
1304
|
t.label
|
|
873
1305
|
] }, t.key)) });
|
|
874
1306
|
App = () => {
|
|
875
|
-
const [tab, setTab] =
|
|
876
|
-
const [focused, setFocused] =
|
|
1307
|
+
const [tab, setTab] = useState8("1");
|
|
1308
|
+
const [focused, setFocused] = useState8(false);
|
|
877
1309
|
const { exit } = useApp();
|
|
878
|
-
|
|
1310
|
+
useInput8((input) => {
|
|
879
1311
|
if (focused) return;
|
|
880
1312
|
if (input === "q") {
|
|
881
1313
|
exit();
|
|
@@ -884,20 +1316,20 @@ var init_App = __esm({
|
|
|
884
1316
|
const found = TABS.find((t) => t.key === input);
|
|
885
1317
|
if (found) setTab(found.key);
|
|
886
1318
|
});
|
|
887
|
-
return /* @__PURE__ */
|
|
888
|
-
/* @__PURE__ */
|
|
889
|
-
/* @__PURE__ */
|
|
890
|
-
/* @__PURE__ */
|
|
1319
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, children: [
|
|
1320
|
+
/* @__PURE__ */ jsxs8(Box8, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
1321
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "wife" }),
|
|
1322
|
+
/* @__PURE__ */ jsx8(Text8, { children: " \u2014 daily mood-lifter control panel" })
|
|
891
1323
|
] }),
|
|
892
|
-
/* @__PURE__ */
|
|
893
|
-
/* @__PURE__ */
|
|
894
|
-
tab === "1" && /* @__PURE__ */
|
|
895
|
-
tab === "2" && /* @__PURE__ */
|
|
896
|
-
tab === "3" && /* @__PURE__ */
|
|
897
|
-
tab === "4" && /* @__PURE__ */
|
|
898
|
-
tab === "5" && /* @__PURE__ */
|
|
1324
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(TabBar, { active: tab }) }),
|
|
1325
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
1326
|
+
tab === "1" && /* @__PURE__ */ jsx8(Home, {}),
|
|
1327
|
+
tab === "2" && /* @__PURE__ */ jsx8(History, {}),
|
|
1328
|
+
tab === "3" && /* @__PURE__ */ jsx8(Settings, { onFocusChange: setFocused }),
|
|
1329
|
+
tab === "4" && /* @__PURE__ */ jsx8(PromptScreen, { onFocusChange: setFocused }),
|
|
1330
|
+
tab === "5" && /* @__PURE__ */ jsx8(ScheduleScreen, { onFocusChange: setFocused })
|
|
899
1331
|
] }),
|
|
900
|
-
/* @__PURE__ */
|
|
1332
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
901
1333
|
"1-5 switch tabs \xB7 q quit",
|
|
902
1334
|
focused ? " \xB7 esc to leave input" : ""
|
|
903
1335
|
] }) })
|
|
@@ -998,9 +1430,9 @@ var registerInit = (program2) => {
|
|
|
998
1430
|
|
|
999
1431
|
// src/cli/commands/setup.ts
|
|
1000
1432
|
import { spawnSync } from "child_process";
|
|
1001
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
1433
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
1002
1434
|
import { homedir as homedir2 } from "os";
|
|
1003
|
-
import { dirname as
|
|
1435
|
+
import { dirname as dirname6, join as join2 } from "path";
|
|
1004
1436
|
import { fileURLToPath } from "url";
|
|
1005
1437
|
import { createInterface } from "readline";
|
|
1006
1438
|
import qrcode from "qrcode-terminal";
|
|
@@ -1038,6 +1470,7 @@ var saveQrImage = (dataUrl) => {
|
|
|
1038
1470
|
|
|
1039
1471
|
// src/cli/commands/setup.ts
|
|
1040
1472
|
init_paths();
|
|
1473
|
+
init_compose();
|
|
1041
1474
|
init_config();
|
|
1042
1475
|
init_secrets();
|
|
1043
1476
|
init_schedule();
|
|
@@ -1124,52 +1557,8 @@ var presentQr = (dataUrl, sessionId, baseUrl, apiKey) => {
|
|
|
1124
1557
|
);
|
|
1125
1558
|
console.warn(` | base64 -d > waify-qr.png`);
|
|
1126
1559
|
};
|
|
1127
|
-
var composeTemplate = () => `services:
|
|
1128
|
-
openwa-api:
|
|
1129
|
-
image: ghcr.io/deckasoft/openwa:latest
|
|
1130
|
-
ports:
|
|
1131
|
-
- '2785:2785'
|
|
1132
|
-
environment:
|
|
1133
|
-
- NODE_ENV=production
|
|
1134
|
-
- PORT=2785
|
|
1135
|
-
- DATABASE_TYPE=sqlite
|
|
1136
|
-
- DATABASE_SYNCHRONIZE=true
|
|
1137
|
-
- ENGINE_TYPE=whatsapp-web.js
|
|
1138
|
-
- SESSION_DATA_PATH=/app/data/sessions
|
|
1139
|
-
- PUPPETEER_HEADLESS=true
|
|
1140
|
-
- PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu
|
|
1141
|
-
- STORAGE_TYPE=local
|
|
1142
|
-
- STORAGE_LOCAL_PATH=/app/data/media
|
|
1143
|
-
- QUEUE_ENABLED=false
|
|
1144
|
-
- REDIS_ENABLED=false
|
|
1145
|
-
- REDIS_BUILTIN=false
|
|
1146
|
-
volumes:
|
|
1147
|
-
- openwa-data:/app/data
|
|
1148
|
-
networks:
|
|
1149
|
-
- waify-network
|
|
1150
|
-
restart: unless-stopped
|
|
1151
|
-
|
|
1152
|
-
scheduler:
|
|
1153
|
-
image: mcuadros/ofelia:latest
|
|
1154
|
-
depends_on:
|
|
1155
|
-
- openwa-api
|
|
1156
|
-
command: daemon --config=/etc/ofelia/config.ini
|
|
1157
|
-
volumes:
|
|
1158
|
-
- /var/run/docker.sock:/var/run/docker.sock
|
|
1159
|
-
- ${schedulePath()}:/etc/ofelia/config.ini:ro
|
|
1160
|
-
networks:
|
|
1161
|
-
- waify-network
|
|
1162
|
-
restart: unless-stopped
|
|
1163
|
-
|
|
1164
|
-
volumes:
|
|
1165
|
-
openwa-data:
|
|
1166
|
-
|
|
1167
|
-
networks:
|
|
1168
|
-
waify-network:
|
|
1169
|
-
name: waify-network
|
|
1170
|
-
`;
|
|
1171
1560
|
var waifyVersion = () => {
|
|
1172
|
-
const dir =
|
|
1561
|
+
const dir = dirname6(fileURLToPath(import.meta.url));
|
|
1173
1562
|
const candidates = [
|
|
1174
1563
|
join2(dir, "..", "..", "package.json"),
|
|
1175
1564
|
join2(dir, "..", "..", "..", "package.json")
|
|
@@ -1188,7 +1577,7 @@ ENTRYPOINT ["waify"]
|
|
|
1188
1577
|
`;
|
|
1189
1578
|
var buildSenderImage = () => {
|
|
1190
1579
|
console.warn("Writing Dockerfile and building sender image...");
|
|
1191
|
-
|
|
1580
|
+
writeFileSync7(dockerfilePath(), dockerfileTemplate(waifyVersion()), "utf-8");
|
|
1192
1581
|
const result = spawnSync(
|
|
1193
1582
|
"docker",
|
|
1194
1583
|
["build", "-t", "openwa-scripts-sender:latest", "-f", dockerfilePath(), dataDir()],
|
|
@@ -1228,6 +1617,77 @@ var promptUntilValid = async (promptFn, question, validate, errorMsg) => {
|
|
|
1228
1617
|
process.stderr.write(errorMsg + "\n");
|
|
1229
1618
|
return promptUntilValid(promptFn, question, validate, errorMsg);
|
|
1230
1619
|
};
|
|
1620
|
+
var promptLanguage = async (promptFn) => {
|
|
1621
|
+
process.stderr.write("\nMessage language:\n");
|
|
1622
|
+
LANGUAGES.forEach((l, i) => process.stderr.write(` ${i + 1}) ${l}
|
|
1623
|
+
`));
|
|
1624
|
+
process.stderr.write(` ${LANGUAGES.length + 1}) Other (type your own)
|
|
1625
|
+
`);
|
|
1626
|
+
const answer = (await promptFn(`Choose [1-${LANGUAGES.length + 1}] (default 1 \u2014 Spanish): `)).trim();
|
|
1627
|
+
if (answer === "") return "Spanish";
|
|
1628
|
+
const n = Number(answer);
|
|
1629
|
+
if (Number.isInteger(n)) {
|
|
1630
|
+
if (n >= 1 && n <= LANGUAGES.length) return LANGUAGES[n - 1];
|
|
1631
|
+
if (n === LANGUAGES.length + 1) {
|
|
1632
|
+
const custom = (await promptFn("Language name: ")).trim();
|
|
1633
|
+
return custom || "Spanish";
|
|
1634
|
+
}
|
|
1635
|
+
return "Spanish";
|
|
1636
|
+
}
|
|
1637
|
+
return answer;
|
|
1638
|
+
};
|
|
1639
|
+
var promptTimezone = async (promptFn) => {
|
|
1640
|
+
const detected = detectTimezone();
|
|
1641
|
+
const zones = new Set(supportedTimezones());
|
|
1642
|
+
process.stderr.write(
|
|
1643
|
+
`
|
|
1644
|
+
Timezone for your schedule (IANA name, e.g. America/Guayaquil). Detected: ${detected}
|
|
1645
|
+
`
|
|
1646
|
+
);
|
|
1647
|
+
const ask = async () => {
|
|
1648
|
+
const answer = (await promptFn(`Timezone [${detected}]: `)).trim();
|
|
1649
|
+
if (answer === "") return zones.has(detected) ? detected : "UTC";
|
|
1650
|
+
if (zones.has(answer)) return answer;
|
|
1651
|
+
process.stderr.write(`"${answer}" is not a valid IANA zone. Try e.g. Europe/Madrid, America/Sao_Paulo.
|
|
1652
|
+
`);
|
|
1653
|
+
return ask();
|
|
1654
|
+
};
|
|
1655
|
+
return ask();
|
|
1656
|
+
};
|
|
1657
|
+
var FREQUENCY_BY_CHOICE = {
|
|
1658
|
+
"1": "daily",
|
|
1659
|
+
"2": "weekdays",
|
|
1660
|
+
"3": "weekends",
|
|
1661
|
+
"4": "custom"
|
|
1662
|
+
};
|
|
1663
|
+
var HHMM_RE = /^([01]?\d|2[0-3]):([0-5]\d)$/;
|
|
1664
|
+
var promptJobScheduleCron = async (promptFn) => {
|
|
1665
|
+
const time = await promptUntilValid(
|
|
1666
|
+
promptFn,
|
|
1667
|
+
"Time (HH:MM, 24h): ",
|
|
1668
|
+
(v) => HHMM_RE.test(v),
|
|
1669
|
+
"Invalid time. Use 24h HH:MM, e.g. 09:00 or 19:30."
|
|
1670
|
+
);
|
|
1671
|
+
const [hourStr, minuteStr] = time.split(":");
|
|
1672
|
+
const hour = Number(hourStr);
|
|
1673
|
+
const minute = Number(minuteStr);
|
|
1674
|
+
process.stderr.write("Frequency: 1) Daily 2) Weekdays 3) Weekends 4) Custom days\n");
|
|
1675
|
+
const freqChoice = await promptUntilValid(
|
|
1676
|
+
promptFn,
|
|
1677
|
+
"Choose [1-4] (default 1 \u2014 Daily): ",
|
|
1678
|
+
(v) => v === "" || v in FREQUENCY_BY_CHOICE,
|
|
1679
|
+
"Choose 1, 2, 3, or 4."
|
|
1680
|
+
);
|
|
1681
|
+
const frequency = freqChoice === "" ? "daily" : FREQUENCY_BY_CHOICE[freqChoice];
|
|
1682
|
+
if (frequency !== "custom") return buildCron({ hour, minute, frequency });
|
|
1683
|
+
const daysAnswer = await promptUntilValid(
|
|
1684
|
+
promptFn,
|
|
1685
|
+
"Days (e.g. mon,wed,fri): ",
|
|
1686
|
+
(v) => parseDays(v) !== null,
|
|
1687
|
+
`Invalid days. Use ${DAY_LABELS.map((d) => d.toLowerCase()).join(",")} or 0-6, comma-separated.`
|
|
1688
|
+
);
|
|
1689
|
+
return buildCron({ hour, minute, frequency, days: parseDays(daysAnswer) });
|
|
1690
|
+
};
|
|
1231
1691
|
var collectJobs = async (promptFn, accumulated = []) => {
|
|
1232
1692
|
const name = await promptUntilValid(
|
|
1233
1693
|
promptFn,
|
|
@@ -1235,12 +1695,7 @@ var collectJobs = async (promptFn, accumulated = []) => {
|
|
|
1235
1695
|
(v) => /^[a-z0-9-]+$/.test(v),
|
|
1236
1696
|
"Name must be lowercase letters, numbers, and dashes only."
|
|
1237
1697
|
);
|
|
1238
|
-
const schedule = await
|
|
1239
|
-
promptFn,
|
|
1240
|
-
"Cron pattern (e.g. 0 0 9 * * *): ",
|
|
1241
|
-
isValidCron,
|
|
1242
|
-
"Invalid cron pattern. Use 6 space-separated fields, e.g. 0 0 9 * * *"
|
|
1243
|
-
);
|
|
1698
|
+
const schedule = await promptJobScheduleCron(promptFn);
|
|
1244
1699
|
const job = ScheduledJobSchema.parse({ name, schedule, command: "send" });
|
|
1245
1700
|
const jobs = [...accumulated, job];
|
|
1246
1701
|
const more = (await promptFn("Add another schedule? (y/N) ")).trim().toLowerCase();
|
|
@@ -1248,7 +1703,7 @@ var collectJobs = async (promptFn, accumulated = []) => {
|
|
|
1248
1703
|
};
|
|
1249
1704
|
var promptScheduleJobs = async (promptFn) => {
|
|
1250
1705
|
process.stderr.write(
|
|
1251
|
-
"\nConfigure your message schedule (at least one job required).\nJob names: lowercase letters, numbers, and dashes only.\
|
|
1706
|
+
"\nConfigure your message schedule (at least one job required).\nJob names: lowercase letters, numbers, and dashes only.\nPick a time and frequency for each \u2014 cron is generated for you.\n\n"
|
|
1252
1707
|
);
|
|
1253
1708
|
return collectJobs(promptFn);
|
|
1254
1709
|
};
|
|
@@ -1271,7 +1726,7 @@ var registerSetup = (program2) => {
|
|
|
1271
1726
|
return;
|
|
1272
1727
|
}
|
|
1273
1728
|
console.warn("Creating config directory...");
|
|
1274
|
-
|
|
1729
|
+
mkdirSync6(join2(homedir2(), ".config", "waify"), { recursive: true });
|
|
1275
1730
|
const baseUrl = loadConfig().openwaBaseUrl;
|
|
1276
1731
|
let geminiKey = "";
|
|
1277
1732
|
while (!geminiKey.trim()) {
|
|
@@ -1297,11 +1752,13 @@ var registerSetup = (program2) => {
|
|
|
1297
1752
|
}
|
|
1298
1753
|
}
|
|
1299
1754
|
const chatId = `${recipientNumber.trim()}@c.us`;
|
|
1755
|
+
const language = await promptLanguage((q) => promptLine(rl, q));
|
|
1756
|
+
const timezone = await promptTimezone((q) => promptLine(rl, q));
|
|
1300
1757
|
saveSecrets({ GEMINI_API_KEY: geminiKey.trim(), OPENWA_API_KEY: "" });
|
|
1301
|
-
saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
|
|
1758
|
+
saveConfig({ ...loadConfig(), recipients: [{ chatId }], language, timezone });
|
|
1302
1759
|
const jobs = await promptScheduleJobs((q) => promptLine(rl, q));
|
|
1303
1760
|
console.warn("Writing docker-compose.yml...");
|
|
1304
|
-
|
|
1761
|
+
writeCompose(timezone);
|
|
1305
1762
|
console.warn(
|
|
1306
1763
|
"Starting OpenWA containers (this may take a minute on first run)..."
|
|
1307
1764
|
);
|
|
@@ -1572,7 +2029,7 @@ var registerSend = (program2) => {
|
|
|
1572
2029
|
assertConfigReady(config);
|
|
1573
2030
|
const prompt = loadPrompt();
|
|
1574
2031
|
const provider = createGeminiProvider({ apiKey: secrets.GEMINI_API_KEY });
|
|
1575
|
-
const text = await generateMessage({ provider, prompt });
|
|
2032
|
+
const text = await generateMessage({ provider, prompt, language: config.language });
|
|
1576
2033
|
await sendMessage({
|
|
1577
2034
|
baseUrl: config.openwaBaseUrl,
|
|
1578
2035
|
apiKey: secrets.OPENWA_API_KEY,
|
|
@@ -1591,17 +2048,19 @@ var registerSend = (program2) => {
|
|
|
1591
2048
|
};
|
|
1592
2049
|
|
|
1593
2050
|
// src/cli/commands/preview.ts
|
|
2051
|
+
init_config();
|
|
1594
2052
|
init_secrets();
|
|
1595
2053
|
init_prompt();
|
|
1596
2054
|
init_gemini();
|
|
1597
2055
|
var registerPreview = (program2) => {
|
|
1598
2056
|
program2.command("preview").description("Generate a candidate message and print it to stdout (no send)").option("-n, --count <n>", "generate N candidates", "1").action(async ({ count }) => {
|
|
1599
2057
|
const secrets = loadSecrets();
|
|
2058
|
+
const config = loadConfig();
|
|
1600
2059
|
const prompt = loadPrompt();
|
|
1601
2060
|
const provider = createGeminiProvider({ apiKey: secrets.GEMINI_API_KEY });
|
|
1602
2061
|
const n = Math.max(1, parseInt(count, 10) || 1);
|
|
1603
2062
|
const messages = await Promise.all(
|
|
1604
|
-
Array.from({ length: n }, () => generateMessage({ provider, prompt }))
|
|
2063
|
+
Array.from({ length: n }, () => generateMessage({ provider, prompt, language: config.language }))
|
|
1605
2064
|
);
|
|
1606
2065
|
messages.forEach((m, i) => {
|
|
1607
2066
|
if (n > 1) console.warn(`--- candidate ${i + 1} ---`);
|
|
@@ -1754,15 +2213,13 @@ var registerPrompt = (program2) => {
|
|
|
1754
2213
|
// src/cli/commands/schedule.ts
|
|
1755
2214
|
init_schedule();
|
|
1756
2215
|
init_paths();
|
|
1757
|
-
|
|
1758
|
-
var
|
|
2216
|
+
init_scheduler();
|
|
2217
|
+
var restartScheduler2 = async () => {
|
|
1759
2218
|
console.warn("restarting scheduler\u2026");
|
|
1760
|
-
const result =
|
|
1761
|
-
|
|
1762
|
-
});
|
|
1763
|
-
if (result.status !== 0) {
|
|
2219
|
+
const result = await restartScheduler();
|
|
2220
|
+
if (!result.ok) {
|
|
1764
2221
|
console.warn(
|
|
1765
|
-
`warning: scheduler restart failed \u2014 run manually: docker compose -f ${composePath()}
|
|
2222
|
+
`warning: scheduler restart failed \u2014 run manually: docker compose -f ${composePath()} up -d --force-recreate scheduler`
|
|
1766
2223
|
);
|
|
1767
2224
|
}
|
|
1768
2225
|
};
|
|
@@ -1779,16 +2236,16 @@ var registerSchedule = (program2) => {
|
|
|
1779
2236
|
`);
|
|
1780
2237
|
});
|
|
1781
2238
|
});
|
|
1782
|
-
schedule.command("add <name> <cron>").description('Add a new job. <cron> uses 6-field syntax (sec min hour dom month dow), e.g. "0 0 9 * * *"').option("-c, --command <cmd>", "command for the sender container to run", "send").option("--no-restart", "skip automatic scheduler restart").action((name, cron, { command, restart }) => {
|
|
2239
|
+
schedule.command("add <name> <cron>").description('Add a new job. <cron> uses 6-field syntax (sec min hour dom month dow), e.g. "0 0 9 * * *"').option("-c, --command <cmd>", "command for the sender container to run", "send").option("--no-restart", "skip automatic scheduler restart").action(async (name, cron, { command, restart }) => {
|
|
1783
2240
|
const job = ScheduledJobSchema.parse({ name, schedule: cron, command });
|
|
1784
2241
|
addJob(job);
|
|
1785
2242
|
console.warn(`added job "${name}"`);
|
|
1786
|
-
if (restart)
|
|
2243
|
+
if (restart) await restartScheduler2();
|
|
1787
2244
|
});
|
|
1788
|
-
schedule.command("remove <name>").description("Remove a job by name").option("--no-restart", "skip automatic scheduler restart").action((name, { restart }) => {
|
|
2245
|
+
schedule.command("remove <name>").description("Remove a job by name").option("--no-restart", "skip automatic scheduler restart").action(async (name, { restart }) => {
|
|
1789
2246
|
removeJob(name);
|
|
1790
2247
|
console.warn(`removed job "${name}"`);
|
|
1791
|
-
if (restart)
|
|
2248
|
+
if (restart) await restartScheduler2();
|
|
1792
2249
|
});
|
|
1793
2250
|
};
|
|
1794
2251
|
|