@deckasoft/waify 0.4.0 → 0.4.1

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 +99 -39
  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
  `;
@@ -953,19 +954,50 @@ var registerInit = (program2) => {
953
954
  };
954
955
 
955
956
  // src/cli/commands/setup.ts
956
- init_paths();
957
- init_config();
958
- init_secrets();
959
- init_schedule();
960
- init_prompt();
961
957
  import { spawnSync } from "child_process";
962
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
958
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
963
959
  import { homedir as homedir2 } from "os";
964
960
  import { join as join2 } from "path";
965
961
  import { createInterface } from "readline";
966
962
  import qrcode from "qrcode-terminal";
963
+
964
+ // src/core/qr.ts
965
+ init_paths();
966
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
967
+ import { dirname as dirname4 } from "path";
967
968
  import { PNG } from "pngjs";
968
969
  import jsQR from "jsqr";
970
+ var stripDataUrlPrefix = (dataUrl) => dataUrl.replace(/^data:image\/\w+;base64,/, "");
971
+ var decodeQrDataUrl = (dataUrl) => {
972
+ try {
973
+ const buffer = Buffer.from(stripDataUrlPrefix(dataUrl), "base64");
974
+ const png = PNG.sync.read(buffer);
975
+ const result = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
976
+ return result?.data ?? null;
977
+ } catch {
978
+ return null;
979
+ }
980
+ };
981
+ var saveQrImage = (dataUrl) => {
982
+ try {
983
+ const buffer = Buffer.from(stripDataUrlPrefix(dataUrl), "base64");
984
+ const path = qrImagePath();
985
+ mkdirSync4(dirname4(path), { recursive: true });
986
+ writeFileSync4(path, buffer);
987
+ return path;
988
+ } catch (err) {
989
+ const msg = err instanceof Error ? err.message : String(err);
990
+ console.warn(`Could not save QR image: ${msg}`);
991
+ return null;
992
+ }
993
+ };
994
+
995
+ // src/cli/commands/setup.ts
996
+ init_paths();
997
+ init_config();
998
+ init_secrets();
999
+ init_schedule();
1000
+ init_prompt();
969
1001
  import { z as z5 } from "zod";
970
1002
  var SessionResponseSchema = z5.object({
971
1003
  id: z5.string().optional(),
@@ -1020,24 +1052,34 @@ var createSpinner = (message) => {
1020
1052
  };
1021
1053
  var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1022
1054
  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) => {
1055
+ var renderQrInTerminal = (dataUrl) => {
1031
1056
  const raw = decodeQrDataUrl(dataUrl);
1032
- if (raw) {
1033
- qrcode.generate(raw, { small: true }, () => resolve2());
1034
- } else {
1057
+ if (!raw) return false;
1058
+ qrcode.generate(raw, { small: true });
1059
+ return true;
1060
+ };
1061
+ var presentQr = (dataUrl, sessionId, baseUrl, apiKey) => {
1062
+ const rendered = renderQrInTerminal(dataUrl);
1063
+ if (!rendered) {
1035
1064
  console.warn(
1036
- " (Could not decode QR image \u2014 try scanning from the API URL instead)"
1065
+ " (Could not decode QR image \u2014 use the saved PNG or curl command below)"
1037
1066
  );
1038
- resolve2();
1039
1067
  }
1040
- });
1068
+ const savedPath = saveQrImage(dataUrl);
1069
+ if (savedPath) {
1070
+ console.warn(`
1071
+ QR also saved to: ${savedPath}`);
1072
+ console.warn(` Open it with: open ${savedPath}`);
1073
+ }
1074
+ console.warn("\n To re-fetch the QR if it expires:");
1075
+ console.warn(
1076
+ ` curl -s -H "X-API-Key: ${apiKey}" ${baseUrl}/api/sessions/${sessionId}/qr \\`
1077
+ );
1078
+ console.warn(
1079
+ ` | sed 's/.*"qrCode":"data:image\\/png;base64,//;s/".*//' \\`
1080
+ );
1081
+ console.warn(` | base64 -d > waify-qr.png`);
1082
+ };
1041
1083
  var composeTemplate = () => `services:
1042
1084
  openwa-api:
1043
1085
  image: ghcr.io/deckasoft/openwa:latest
@@ -1064,6 +1106,16 @@ var composeTemplate = () => `services:
1064
1106
  volumes:
1065
1107
  openwa-data:
1066
1108
  `;
1109
+ var finalizeSetup = (sessionId) => {
1110
+ saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
1111
+ if (!existsSync6(promptPath())) {
1112
+ savePrompt(defaultPrompt);
1113
+ }
1114
+ if (!existsSync6(scheduleJsonPath())) {
1115
+ saveSchedule(defaultSchedule);
1116
+ }
1117
+ console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
1118
+ };
1067
1119
  var promptLine = (rl, question) => new Promise((resolve2) => rl.question(question, resolve2));
1068
1120
  var registerSetup = (program2) => {
1069
1121
  program2.command("setup").description(
@@ -1084,7 +1136,7 @@ var registerSetup = (program2) => {
1084
1136
  return;
1085
1137
  }
1086
1138
  console.warn("Creating config directory...");
1087
- mkdirSync4(join2(homedir2(), ".config", "waify"), { recursive: true });
1139
+ mkdirSync5(join2(homedir2(), ".config", "waify"), { recursive: true });
1088
1140
  const baseUrl = loadConfig().openwaBaseUrl;
1089
1141
  let geminiKey = "";
1090
1142
  while (!geminiKey.trim()) {
@@ -1113,7 +1165,7 @@ var registerSetup = (program2) => {
1113
1165
  saveSecrets({ GEMINI_API_KEY: geminiKey.trim(), OPENWA_API_KEY: "" });
1114
1166
  saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
1115
1167
  console.warn("Writing docker-compose.yml...");
1116
- writeFileSync5(composePath(), composeTemplate(), "utf-8");
1168
+ writeFileSync6(composePath(), composeTemplate(), "utf-8");
1117
1169
  console.warn(
1118
1170
  "Starting OpenWA containers (this may take a minute on first run)..."
1119
1171
  );
@@ -1261,6 +1313,23 @@ var registerSetup = (program2) => {
1261
1313
  `Failed to start session: ${startRes.status} ${startRes.statusText}`
1262
1314
  );
1263
1315
  }
1316
+ try {
1317
+ const preStatusRes = await fetchWithTimeout(
1318
+ `${baseUrl}/api/sessions/${sessionId}`,
1319
+ { headers: { "X-API-Key": openwaApiKey } }
1320
+ );
1321
+ if (preStatusRes.ok) {
1322
+ const preStatus = StatusResponseSchema.safeParse(
1323
+ await preStatusRes.json()
1324
+ );
1325
+ if (preStatus.success && preStatus.data.status === "ready") {
1326
+ console.warn("\u2713 WhatsApp already linked \u2014 skipping QR scan");
1327
+ finalizeSetup(sessionId);
1328
+ return;
1329
+ }
1330
+ }
1331
+ } catch {
1332
+ }
1264
1333
  const qrSpinner = createSpinner("Waiting for QR code (Chromium is starting)...");
1265
1334
  let qrCode;
1266
1335
  const qrStart = Date.now();
@@ -1291,7 +1360,7 @@ var registerSetup = (program2) => {
1291
1360
  );
1292
1361
  console.warn(" Settings \u2192 Linked Devices \u2192 Link a Device\n");
1293
1362
  if (qrCode) {
1294
- await renderQrInTerminal(qrCode);
1363
+ presentQr(qrCode, sessionId, baseUrl, openwaApiKey);
1295
1364
  console.warn(
1296
1365
  "\n (QR expires in ~20s \u2014 re-run setup if it expires before you scan)"
1297
1366
  );
@@ -1332,16 +1401,7 @@ var registerSetup = (program2) => {
1332
1401
  return;
1333
1402
  }
1334
1403
  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
- );
1404
+ finalizeSetup(sessionId);
1345
1405
  } catch (err) {
1346
1406
  console.error(err instanceof Error ? err.message : String(err));
1347
1407
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckasoft/waify",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",