@deckasoft/waify 0.4.0 → 0.4.2

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 +123 -51
  2. package/package.json +3 -1
package/dist/cli/index.js CHANGED
@@ -12,7 +12,7 @@ var __export = (target, all) => {
12
12
  // src/core/paths.ts
13
13
  import { join } from "path";
14
14
  import { homedir } from "os";
15
- var dataDir, configPath, promptPath, scheduleJsonPath, schedulePath, logPath, envPath, composePath;
15
+ var dataDir, configPath, promptPath, scheduleJsonPath, schedulePath, logPath, envPath, composePath, qrImagePath;
16
16
  var init_paths = __esm({
17
17
  "src/core/paths.ts"() {
18
18
  "use strict";
@@ -24,6 +24,7 @@ var init_paths = __esm({
24
24
  logPath = () => join(dataDir(), "messages.log");
25
25
  envPath = () => process.env["WAIFY_ENV_PATH"] ?? join(dataDir(), ".env");
26
26
  composePath = () => join(dataDir(), "docker-compose.yml");
27
+ qrImagePath = () => join(dataDir(), "qr.png");
27
28
  }
28
29
  });
29
30
 
@@ -203,7 +204,7 @@ var init_schedule = __esm({
203
204
  });
204
205
 
205
206
  // src/core/secrets.ts
206
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
207
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
207
208
  import { z as z4 } from "zod";
208
209
  var SecretsSchema, parseEnvFile, loadSecrets, tryLoadSecrets, saveSecrets;
209
210
  var init_secrets = __esm({
@@ -231,7 +232,7 @@ var init_secrets = __esm({
231
232
  const existing = existsSync5(path) ? parseEnvFile(readFileSync4(path, "utf-8")) : {};
232
233
  const merged = { ...existing, ...next };
233
234
  const body = Object.entries(merged).filter(([, v]) => typeof v === "string" && v.length > 0).map(([k, v]) => `${k}=${v}`).join("\n");
234
- writeFileSync4(path, body + "\n", "utf-8");
235
+ writeFileSync5(path, body + "\n", "utf-8");
235
236
  };
236
237
  }
237
238
  });
@@ -307,8 +308,8 @@ var init_sender = __esm({
307
308
  });
308
309
 
309
310
  // src/core/logger.ts
310
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5 } from "fs";
311
- import { dirname as dirname4 } from "path";
311
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5 } from "fs";
312
+ import { dirname as dirname5 } from "path";
312
313
  var log, LINE_RE, parseLine, readHistory;
313
314
  var init_logger = __esm({
314
315
  "src/core/logger.ts"() {
@@ -316,7 +317,7 @@ var init_logger = __esm({
316
317
  init_paths();
317
318
  log = (status, detail) => {
318
319
  const path = logPath();
319
- mkdirSync5(dirname4(path), { recursive: true });
320
+ mkdirSync6(dirname5(path), { recursive: true });
320
321
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
321
322
  const line = `[${timestamp}] ${status.toUpperCase()} | ${detail}
322
323
  `;
@@ -887,7 +888,8 @@ var init_start = __esm({
887
888
  });
888
889
 
889
890
  // src/cli/index.ts
890
- import "dotenv/config";
891
+ init_paths();
892
+ import { config as dotenvConfig } from "dotenv";
891
893
  import { Command } from "commander";
892
894
 
893
895
  // src/cli/commands/init.ts
@@ -953,19 +955,50 @@ var registerInit = (program2) => {
953
955
  };
954
956
 
955
957
  // src/cli/commands/setup.ts
956
- init_paths();
957
- init_config();
958
- init_secrets();
959
- init_schedule();
960
- init_prompt();
961
958
  import { spawnSync } from "child_process";
962
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
959
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
963
960
  import { homedir as homedir2 } from "os";
964
961
  import { join as join2 } from "path";
965
962
  import { createInterface } from "readline";
966
963
  import qrcode from "qrcode-terminal";
964
+
965
+ // src/core/qr.ts
966
+ init_paths();
967
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
968
+ import { dirname as dirname4 } from "path";
967
969
  import { PNG } from "pngjs";
968
970
  import jsQR from "jsqr";
971
+ var stripDataUrlPrefix = (dataUrl) => dataUrl.replace(/^data:image\/\w+;base64,/, "");
972
+ var decodeQrDataUrl = (dataUrl) => {
973
+ try {
974
+ const buffer = Buffer.from(stripDataUrlPrefix(dataUrl), "base64");
975
+ const png = PNG.sync.read(buffer);
976
+ const result = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
977
+ return result?.data ?? null;
978
+ } catch {
979
+ return null;
980
+ }
981
+ };
982
+ var saveQrImage = (dataUrl) => {
983
+ try {
984
+ const buffer = Buffer.from(stripDataUrlPrefix(dataUrl), "base64");
985
+ const path = qrImagePath();
986
+ mkdirSync4(dirname4(path), { recursive: true });
987
+ writeFileSync4(path, buffer);
988
+ return path;
989
+ } catch (err) {
990
+ const msg = err instanceof Error ? err.message : String(err);
991
+ console.warn(`Could not save QR image: ${msg}`);
992
+ return null;
993
+ }
994
+ };
995
+
996
+ // src/cli/commands/setup.ts
997
+ init_paths();
998
+ init_config();
999
+ init_secrets();
1000
+ init_schedule();
1001
+ init_prompt();
969
1002
  import { z as z5 } from "zod";
970
1003
  var SessionResponseSchema = z5.object({
971
1004
  id: z5.string().optional(),
@@ -1020,24 +1053,34 @@ var createSpinner = (message) => {
1020
1053
  };
1021
1054
  var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1022
1055
  var fetchWithTimeout = (url, opts = {}, timeoutMs = 5e3) => fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
1023
- var decodeQrDataUrl = (dataUrl) => {
1024
- const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, "");
1025
- const buffer = Buffer.from(base64, "base64");
1026
- const png = PNG.sync.read(buffer);
1027
- const result = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
1028
- return result?.data ?? null;
1029
- };
1030
- var renderQrInTerminal = (dataUrl) => new Promise((resolve2) => {
1056
+ var renderQrInTerminal = (dataUrl) => {
1031
1057
  const raw = decodeQrDataUrl(dataUrl);
1032
- if (raw) {
1033
- qrcode.generate(raw, { small: true }, () => resolve2());
1034
- } else {
1058
+ if (!raw) return false;
1059
+ qrcode.generate(raw, { small: true });
1060
+ return true;
1061
+ };
1062
+ var presentQr = (dataUrl, sessionId, baseUrl, apiKey) => {
1063
+ const rendered = renderQrInTerminal(dataUrl);
1064
+ if (!rendered) {
1035
1065
  console.warn(
1036
- " (Could not decode QR image \u2014 try scanning from the API URL instead)"
1066
+ " (Could not decode QR image \u2014 use the saved PNG or curl command below)"
1037
1067
  );
1038
- resolve2();
1039
1068
  }
1040
- });
1069
+ const savedPath = saveQrImage(dataUrl);
1070
+ if (savedPath) {
1071
+ console.warn(`
1072
+ QR also saved to: ${savedPath}`);
1073
+ console.warn(` Open it with: open ${savedPath}`);
1074
+ }
1075
+ console.warn("\n To re-fetch the QR if it expires:");
1076
+ console.warn(
1077
+ ` curl -s -H "X-API-Key: ${apiKey}" ${baseUrl}/api/sessions/${sessionId}/qr \\`
1078
+ );
1079
+ console.warn(
1080
+ ` | sed 's/.*"qrCode":"data:image\\/png;base64,//;s/".*//' \\`
1081
+ );
1082
+ console.warn(` | base64 -d > waify-qr.png`);
1083
+ };
1041
1084
  var composeTemplate = () => `services:
1042
1085
  openwa-api:
1043
1086
  image: ghcr.io/deckasoft/openwa:latest
@@ -1064,6 +1107,16 @@ var composeTemplate = () => `services:
1064
1107
  volumes:
1065
1108
  openwa-data:
1066
1109
  `;
1110
+ var finalizeSetup = (sessionId) => {
1111
+ saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
1112
+ if (!existsSync6(promptPath())) {
1113
+ savePrompt(defaultPrompt);
1114
+ }
1115
+ if (!existsSync6(scheduleJsonPath())) {
1116
+ saveSchedule(defaultSchedule);
1117
+ }
1118
+ console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
1119
+ };
1067
1120
  var promptLine = (rl, question) => new Promise((resolve2) => rl.question(question, resolve2));
1068
1121
  var registerSetup = (program2) => {
1069
1122
  program2.command("setup").description(
@@ -1084,7 +1137,7 @@ var registerSetup = (program2) => {
1084
1137
  return;
1085
1138
  }
1086
1139
  console.warn("Creating config directory...");
1087
- mkdirSync4(join2(homedir2(), ".config", "waify"), { recursive: true });
1140
+ mkdirSync5(join2(homedir2(), ".config", "waify"), { recursive: true });
1088
1141
  const baseUrl = loadConfig().openwaBaseUrl;
1089
1142
  let geminiKey = "";
1090
1143
  while (!geminiKey.trim()) {
@@ -1113,7 +1166,7 @@ var registerSetup = (program2) => {
1113
1166
  saveSecrets({ GEMINI_API_KEY: geminiKey.trim(), OPENWA_API_KEY: "" });
1114
1167
  saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
1115
1168
  console.warn("Writing docker-compose.yml...");
1116
- writeFileSync5(composePath(), composeTemplate(), "utf-8");
1169
+ writeFileSync6(composePath(), composeTemplate(), "utf-8");
1117
1170
  console.warn(
1118
1171
  "Starting OpenWA containers (this may take a minute on first run)..."
1119
1172
  );
@@ -1248,18 +1301,45 @@ var registerSetup = (program2) => {
1248
1301
  { encoding: "utf-8" }
1249
1302
  );
1250
1303
  console.warn("Starting WhatsApp engine...");
1251
- const startRes = await fetchWithTimeout(
1252
- `${baseUrl}/api/sessions/${sessionId}/start`,
1253
- {
1254
- method: "POST",
1255
- headers: { "X-API-Key": openwaApiKey }
1256
- },
1257
- 1e4
1258
- );
1259
- if (!startRes.ok && startRes.status !== 400) {
1260
- throw new Error(
1261
- `Failed to start session: ${startRes.status} ${startRes.statusText}`
1304
+ try {
1305
+ const startRes = await fetchWithTimeout(
1306
+ `${baseUrl}/api/sessions/${sessionId}/start`,
1307
+ {
1308
+ method: "POST",
1309
+ headers: { "X-API-Key": openwaApiKey }
1310
+ },
1311
+ 3e4
1312
+ );
1313
+ if (!startRes.ok && startRes.status !== 400) {
1314
+ throw new Error(
1315
+ `Failed to start session: ${startRes.status} ${startRes.statusText}`
1316
+ );
1317
+ }
1318
+ } catch (err) {
1319
+ if (err instanceof Error && err.name === "TimeoutError") {
1320
+ console.warn(
1321
+ " (Engine start is taking longer than expected \u2014 Chromium may still be loading, continuing\u2026)"
1322
+ );
1323
+ } else {
1324
+ throw err;
1325
+ }
1326
+ }
1327
+ try {
1328
+ const preStatusRes = await fetchWithTimeout(
1329
+ `${baseUrl}/api/sessions/${sessionId}`,
1330
+ { headers: { "X-API-Key": openwaApiKey } }
1262
1331
  );
1332
+ if (preStatusRes.ok) {
1333
+ const preStatus = StatusResponseSchema.safeParse(
1334
+ await preStatusRes.json()
1335
+ );
1336
+ if (preStatus.success && preStatus.data.status === "ready") {
1337
+ console.warn("\u2713 WhatsApp already linked \u2014 skipping QR scan");
1338
+ finalizeSetup(sessionId);
1339
+ return;
1340
+ }
1341
+ }
1342
+ } catch {
1263
1343
  }
1264
1344
  const qrSpinner = createSpinner("Waiting for QR code (Chromium is starting)...");
1265
1345
  let qrCode;
@@ -1291,7 +1371,7 @@ var registerSetup = (program2) => {
1291
1371
  );
1292
1372
  console.warn(" Settings \u2192 Linked Devices \u2192 Link a Device\n");
1293
1373
  if (qrCode) {
1294
- await renderQrInTerminal(qrCode);
1374
+ presentQr(qrCode, sessionId, baseUrl, openwaApiKey);
1295
1375
  console.warn(
1296
1376
  "\n (QR expires in ~20s \u2014 re-run setup if it expires before you scan)"
1297
1377
  );
@@ -1332,16 +1412,7 @@ var registerSetup = (program2) => {
1332
1412
  return;
1333
1413
  }
1334
1414
  connectSpinner.succeed("WhatsApp connected!");
1335
- saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
1336
- if (!existsSync6(promptPath())) {
1337
- savePrompt(defaultPrompt);
1338
- }
1339
- if (!existsSync6(scheduleJsonPath())) {
1340
- saveSchedule(defaultSchedule);
1341
- }
1342
- console.warn(
1343
- "\n\u2713 All done! Run `waify send` to send your first message."
1344
- );
1415
+ finalizeSetup(sessionId);
1345
1416
  } catch (err) {
1346
1417
  console.error(err instanceof Error ? err.message : String(err));
1347
1418
  process.exitCode = 1;
@@ -1580,6 +1651,7 @@ var registerTui = (program2) => {
1580
1651
  };
1581
1652
 
1582
1653
  // src/cli/index.ts
1654
+ dotenvConfig({ path: envPath() });
1583
1655
  var program = new Command();
1584
1656
  program.name("waify").description("AI-powered daily message sender for WhatsApp").version("0.1.0");
1585
1657
  registerInit(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckasoft/waify",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "AI-powered daily message sender for WhatsApp, powered by OpenWA",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -52,8 +52,10 @@
52
52
  "@semantic-release/git": "^10.0.1",
53
53
  "@types/node": "^22.0.0",
54
54
  "@types/pngjs": "^6.0.5",
55
+ "@types/qrcode": "^1.5.6",
55
56
  "@types/qrcode-terminal": "^0.12.2",
56
57
  "@types/react": "^19.2.15",
58
+ "qrcode": "^1.5.4",
57
59
  "semantic-release": "^25.0.3",
58
60
  "tsup": "^8.5.1",
59
61
  "tsx": "^4.19.0",