@deckasoft/waify 0.4.3 → 0.5.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +94 -25
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -119,16 +119,42 @@ var init_prompt = __esm({
119
119
 
120
120
  // src/core/schedule.ts
121
121
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
122
- import { dirname as dirname3, resolve } from "path";
122
+ import { dirname as dirname3 } from "path";
123
123
  import { z as z3 } from "zod";
124
- var ScheduledJobSchema, ScheduleSchema, defaultSchedule, scheduleJsonPath2, loadSchedule, saveSchedule, ofeliaRuntime, renderJob, renderOfeliaIni, regenerateOfeliaIni, addJob, removeJob;
124
+ var CRON_RANGES, STEP_RE, RANGE_RE, isValidCronField, isValidCron, ScheduledJobSchema, ScheduleSchema, defaultSchedule, scheduleJsonPath2, loadSchedule, saveSchedule, ofeliaRuntime, renderJob, renderOfeliaIni, regenerateOfeliaIni, addJob, removeJob;
125
125
  var init_schedule = __esm({
126
126
  "src/core/schedule.ts"() {
127
127
  "use strict";
128
128
  init_paths();
129
+ CRON_RANGES = [
130
+ [0, 59],
131
+ // seconds
132
+ [0, 59],
133
+ // minutes
134
+ [0, 23],
135
+ // hours
136
+ [1, 31],
137
+ // day-of-month
138
+ [1, 12],
139
+ // month
140
+ [0, 7]
141
+ // day-of-week
142
+ ];
143
+ STEP_RE = /^(\*|\d+)\/\d+$/;
144
+ RANGE_RE = /^\d+-\d+$/;
145
+ isValidCronField = (field, [min, max]) => {
146
+ if (field === "*") return true;
147
+ if (STEP_RE.test(field) || RANGE_RE.test(field)) return true;
148
+ const n = Number(field);
149
+ return Number.isInteger(n) && n >= min && n <= max;
150
+ };
151
+ isValidCron = (value) => {
152
+ const fields = value.trim().split(/\s+/);
153
+ return fields.length === 6 && fields.every((f, i) => isValidCronField(f, CRON_RANGES[i]));
154
+ };
129
155
  ScheduledJobSchema = z3.object({
130
156
  name: z3.string().min(1).regex(/^[a-z0-9-]+$/, "name must be lowercase alphanumeric with dashes"),
131
- schedule: z3.string().min(1),
157
+ schedule: z3.string().min(1).refine(isValidCron, { message: "schedule must be a 6-field cron expression (e.g. 0 0 9 * * *)" }),
132
158
  command: z3.string().min(1).default("send")
133
159
  });
134
160
  ScheduleSchema = z3.object({
@@ -156,8 +182,8 @@ var init_schedule = __esm({
156
182
  ofeliaRuntime = () => ({
157
183
  image: process.env["WAIFY_SENDER_IMAGE"] ?? "openwa-scripts-sender:latest",
158
184
  network: process.env["WAIFY_NETWORK"] ?? "waify-network",
159
- hostDataDir: process.env["WAIFY_HOST_DATA_DIR"] ?? resolve(process.cwd(), "data"),
160
- hostEnvFile: process.env["WAIFY_HOST_ENV_FILE"] ?? resolve(process.cwd(), ".env")
185
+ hostDataDir: process.env["WAIFY_HOST_DATA_DIR"] ?? dataDir(),
186
+ hostEnvFile: process.env["WAIFY_HOST_ENV_FILE"] ?? envPath()
161
187
  });
162
188
  renderJob = (job, runtime) => [
163
189
  `[job-run "${job.name}"]`,
@@ -887,8 +913,12 @@ var init_start = __esm({
887
913
  }
888
914
  });
889
915
 
916
+ // src/cli/env.ts
917
+ init_paths();
918
+ import { config as dotenvConfig } from "dotenv";
919
+ dotenvConfig({ path: envPath() });
920
+
890
921
  // src/cli/index.ts
891
- import "dotenv/config";
892
922
  import { Command } from "commander";
893
923
 
894
924
  // src/cli/commands/init.ts
@@ -1050,7 +1080,7 @@ var createSpinner = (message) => {
1050
1080
  }
1051
1081
  };
1052
1082
  };
1053
- var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1083
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1054
1084
  var fetchWithTimeout = (url, opts = {}, timeoutMs = 5e3) => fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
1055
1085
  var renderQrInTerminal = (dataUrl) => {
1056
1086
  const raw = decodeQrDataUrl(dataUrl);
@@ -1106,17 +1136,45 @@ var composeTemplate = () => `services:
1106
1136
  volumes:
1107
1137
  openwa-data:
1108
1138
  `;
1109
- var finalizeSetup = (sessionId) => {
1139
+ var finalizeSetup = (sessionId, jobs) => {
1110
1140
  saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
1111
1141
  if (!existsSync6(promptPath())) {
1112
1142
  savePrompt(defaultPrompt);
1113
1143
  }
1114
- if (!existsSync6(scheduleJsonPath())) {
1115
- saveSchedule(defaultSchedule);
1116
- }
1144
+ saveSchedule({ jobs });
1117
1145
  console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
1118
1146
  };
1119
- var promptLine = (rl, question) => new Promise((resolve2) => rl.question(question, resolve2));
1147
+ var promptLine = (rl, question) => new Promise((resolve) => rl.question(question, resolve));
1148
+ var promptUntilValid = async (promptFn, question, validate, errorMsg) => {
1149
+ const answer = (await promptFn(question)).trim();
1150
+ if (validate(answer)) return answer;
1151
+ process.stderr.write(errorMsg + "\n");
1152
+ return promptUntilValid(promptFn, question, validate, errorMsg);
1153
+ };
1154
+ var collectJobs = async (promptFn, accumulated = []) => {
1155
+ const name = await promptUntilValid(
1156
+ promptFn,
1157
+ "Job name: ",
1158
+ (v) => /^[a-z0-9-]+$/.test(v),
1159
+ "Name must be lowercase letters, numbers, and dashes only."
1160
+ );
1161
+ const schedule = await promptUntilValid(
1162
+ promptFn,
1163
+ "Cron pattern (e.g. 0 0 9 * * *): ",
1164
+ isValidCron,
1165
+ "Invalid cron pattern. Use 6 space-separated fields, e.g. 0 0 9 * * *"
1166
+ );
1167
+ const job = ScheduledJobSchema.parse({ name, schedule, command: "send" });
1168
+ const jobs = [...accumulated, job];
1169
+ const more = (await promptFn("Add another schedule? (y/N) ")).trim().toLowerCase();
1170
+ return more === "y" ? collectJobs(promptFn, jobs) : jobs;
1171
+ };
1172
+ var promptScheduleJobs = async (promptFn) => {
1173
+ process.stderr.write(
1174
+ "\nConfigure your message schedule (at least one job required).\nJob names: lowercase letters, numbers, and dashes only.\nCron pattern: 6 fields, e.g. 0 0 9 * * * (sec min hour dom month dow)\n\n"
1175
+ );
1176
+ return collectJobs(promptFn);
1177
+ };
1120
1178
  var registerSetup = (program2) => {
1121
1179
  program2.command("setup").description(
1122
1180
  "Guided first-run wizard: installs OpenWA, authenticates WhatsApp, and configures waify"
@@ -1164,6 +1222,7 @@ var registerSetup = (program2) => {
1164
1222
  const chatId = `${recipientNumber.trim()}@c.us`;
1165
1223
  saveSecrets({ GEMINI_API_KEY: geminiKey.trim(), OPENWA_API_KEY: "" });
1166
1224
  saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
1225
+ const jobs = await promptScheduleJobs((q) => promptLine(rl, q));
1167
1226
  console.warn("Writing docker-compose.yml...");
1168
1227
  writeFileSync6(composePath(), composeTemplate(), "utf-8");
1169
1228
  console.warn(
@@ -1300,18 +1359,28 @@ var registerSetup = (program2) => {
1300
1359
  { encoding: "utf-8" }
1301
1360
  );
1302
1361
  console.warn("Starting WhatsApp engine...");
1303
- const startRes = await fetchWithTimeout(
1304
- `${baseUrl}/api/sessions/${sessionId}/start`,
1305
- {
1306
- method: "POST",
1307
- headers: { "X-API-Key": openwaApiKey }
1308
- },
1309
- 1e4
1310
- );
1311
- if (!startRes.ok && startRes.status !== 400) {
1312
- throw new Error(
1313
- `Failed to start session: ${startRes.status} ${startRes.statusText}`
1362
+ try {
1363
+ const startRes = await fetchWithTimeout(
1364
+ `${baseUrl}/api/sessions/${sessionId}/start`,
1365
+ {
1366
+ method: "POST",
1367
+ headers: { "X-API-Key": openwaApiKey }
1368
+ },
1369
+ 6e4
1314
1370
  );
1371
+ if (!startRes.ok && startRes.status !== 400) {
1372
+ throw new Error(
1373
+ `Failed to start session: ${startRes.status} ${startRes.statusText}`
1374
+ );
1375
+ }
1376
+ } catch (err) {
1377
+ if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
1378
+ console.warn(
1379
+ " (Engine start is taking longer than expected \u2014 Chromium may still be loading, continuing\u2026)"
1380
+ );
1381
+ } else {
1382
+ throw err;
1383
+ }
1315
1384
  }
1316
1385
  try {
1317
1386
  const preStatusRes = await fetchWithTimeout(
@@ -1324,7 +1393,7 @@ var registerSetup = (program2) => {
1324
1393
  );
1325
1394
  if (preStatus.success && preStatus.data.status === "ready") {
1326
1395
  console.warn("\u2713 WhatsApp already linked \u2014 skipping QR scan");
1327
- finalizeSetup(sessionId);
1396
+ finalizeSetup(sessionId, jobs);
1328
1397
  return;
1329
1398
  }
1330
1399
  }
@@ -1401,7 +1470,7 @@ var registerSetup = (program2) => {
1401
1470
  return;
1402
1471
  }
1403
1472
  connectSpinner.succeed("WhatsApp connected!");
1404
- finalizeSetup(sessionId);
1473
+ finalizeSetup(sessionId, jobs);
1405
1474
  } catch (err) {
1406
1475
  console.error(err instanceof Error ? err.message : String(err));
1407
1476
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckasoft/waify",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "AI-powered daily message sender for WhatsApp, powered by OpenWA",
5
5
  "keywords": [
6
6
  "whatsapp",