@deckasoft/waify 0.3.10 → 0.4.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 +200 -56
  2. package/package.json +4 -1
package/dist/cli/index.js CHANGED
@@ -964,6 +964,8 @@ import { homedir as homedir2 } from "os";
964
964
  import { join as join2 } from "path";
965
965
  import { createInterface } from "readline";
966
966
  import qrcode from "qrcode-terminal";
967
+ import { PNG } from "pngjs";
968
+ import jsQR from "jsqr";
967
969
  import { z as z5 } from "zod";
968
970
  var SessionResponseSchema = z5.object({
969
971
  id: z5.string().optional(),
@@ -976,8 +978,66 @@ var QrResponseSchema = z5.object({
976
978
  var StatusResponseSchema = z5.object({
977
979
  status: z5.string().optional()
978
980
  });
981
+ var SESSION_NAME = "waify";
982
+ var SPINNER_FRAMES = [
983
+ "\u280B",
984
+ "\u2819",
985
+ "\u2839",
986
+ "\u2838",
987
+ "\u283C",
988
+ "\u2834",
989
+ "\u2826",
990
+ "\u2827",
991
+ "\u2807",
992
+ "\u280F"
993
+ ];
994
+ var createSpinner = (message) => {
995
+ let current = message;
996
+ let frame = 0;
997
+ const interval = setInterval(() => {
998
+ process.stderr.write(`\r${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${current}`);
999
+ frame++;
1000
+ }, 80).unref();
1001
+ return {
1002
+ update: (msg) => {
1003
+ current = msg;
1004
+ },
1005
+ succeed: (msg) => {
1006
+ clearInterval(interval);
1007
+ process.stderr.write(`\r\u2713 ${msg}
1008
+ `);
1009
+ },
1010
+ fail: (msg) => {
1011
+ clearInterval(interval);
1012
+ process.stderr.write(`\r\u2717 ${msg}
1013
+ `);
1014
+ },
1015
+ stop: () => {
1016
+ clearInterval(interval);
1017
+ process.stderr.write("\r\x1B[K");
1018
+ }
1019
+ };
1020
+ };
979
1021
  var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
980
1022
  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) => {
1031
+ const raw = decodeQrDataUrl(dataUrl);
1032
+ if (raw) {
1033
+ qrcode.generate(raw, { small: true }, () => resolve2());
1034
+ } else {
1035
+ console.warn(
1036
+ " (Could not decode QR image \u2014 try scanning from the API URL instead)"
1037
+ );
1038
+ resolve2();
1039
+ }
1040
+ });
981
1041
  var composeTemplate = () => `services:
982
1042
  openwa-api:
983
1043
  image: ghcr.io/deckasoft/openwa:latest
@@ -1005,15 +1065,21 @@ volumes:
1005
1065
  openwa-data:
1006
1066
  `;
1007
1067
  var promptLine = (rl, question) => new Promise((resolve2) => rl.question(question, resolve2));
1008
- var renderQr = (qrString) => new Promise((resolve2) => qrcode.generate(qrString, { small: true }, () => resolve2()));
1009
1068
  var registerSetup = (program2) => {
1010
- program2.command("setup").description("Guided first-run wizard: installs OpenWA, authenticates WhatsApp, and configures waify").action(async () => {
1011
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1069
+ program2.command("setup").description(
1070
+ "Guided first-run wizard: installs OpenWA, authenticates WhatsApp, and configures waify"
1071
+ ).action(async () => {
1072
+ const rl = createInterface({
1073
+ input: process.stdin,
1074
+ output: process.stdout
1075
+ });
1012
1076
  try {
1013
1077
  console.warn("Checking Docker...");
1014
1078
  const dockerCheck = spawnSync("docker", ["info"], { stdio: "pipe" });
1015
1079
  if (dockerCheck.status !== 0) {
1016
- console.error("Docker is not running or not installed. Please install Docker and start it before running setup.");
1080
+ console.error(
1081
+ "Docker is not running or not installed. Please install Docker and start it before running setup."
1082
+ );
1017
1083
  process.exitCode = 1;
1018
1084
  return;
1019
1085
  }
@@ -1038,7 +1104,9 @@ var registerSetup = (program2) => {
1038
1104
  "Enter your recipient's WhatsApp number (e.g. 5511999998888 \u2014 digits only, no + or spaces):\n> "
1039
1105
  );
1040
1106
  if (!phoneRegex.test(recipientNumber.trim())) {
1041
- console.warn("Invalid number format. Use digits only, 8\u201315 characters. Please try again.");
1107
+ console.warn(
1108
+ "Invalid number format. Use digits only, 8\u201315 characters. Please try again."
1109
+ );
1042
1110
  }
1043
1111
  }
1044
1112
  const chatId = `${recipientNumber.trim()}@c.us`;
@@ -1046,16 +1114,32 @@ var registerSetup = (program2) => {
1046
1114
  saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
1047
1115
  console.warn("Writing docker-compose.yml...");
1048
1116
  writeFileSync5(composePath(), composeTemplate(), "utf-8");
1049
- console.warn("Starting OpenWA containers (this may take a minute on first run)...");
1050
- const upResult = spawnSync("docker", ["compose", "-f", composePath(), "up", "-d", "--no-deps", "openwa-api"], {
1051
- stdio: "inherit"
1052
- });
1117
+ console.warn(
1118
+ "Starting OpenWA containers (this may take a minute on first run)..."
1119
+ );
1120
+ const upResult = spawnSync(
1121
+ "docker",
1122
+ [
1123
+ "compose",
1124
+ "-f",
1125
+ composePath(),
1126
+ "up",
1127
+ "-d",
1128
+ "--no-deps",
1129
+ "openwa-api"
1130
+ ],
1131
+ {
1132
+ stdio: "inherit"
1133
+ }
1134
+ );
1053
1135
  if (upResult.status !== 0) {
1054
- console.error("Failed to start OpenWA containers. Check docker compose logs for details.");
1136
+ console.error(
1137
+ "Failed to start OpenWA containers. Check docker compose logs for details."
1138
+ );
1055
1139
  process.exitCode = 1;
1056
1140
  return;
1057
1141
  }
1058
- console.warn("Waiting for OpenWA API to start...");
1142
+ const apiSpinner = createSpinner("Waiting for OpenWA API to start...");
1059
1143
  let apiReady = false;
1060
1144
  for (let attempt = 0; attempt < 30; attempt++) {
1061
1145
  try {
@@ -1069,53 +1153,84 @@ var registerSetup = (program2) => {
1069
1153
  await wait(2e3);
1070
1154
  }
1071
1155
  if (!apiReady) {
1072
- console.error(
1073
- "OpenWA API did not become ready in time. Check logs with: docker compose -f " + composePath() + " logs openwa-api"
1156
+ apiSpinner.fail(
1157
+ `OpenWA API did not become ready in time. Check logs with: docker compose -f ${composePath()} logs openwa-api`
1074
1158
  );
1075
1159
  process.exitCode = 1;
1076
1160
  return;
1077
1161
  }
1162
+ apiSpinner.succeed("OpenWA API is ready");
1078
1163
  console.warn("Reading API key from container...");
1079
1164
  const keyResult = spawnSync(
1080
1165
  "docker",
1081
- ["compose", "-f", composePath(), "exec", "-T", "openwa-api", "cat", "/app/data/.api-key"],
1166
+ [
1167
+ "compose",
1168
+ "-f",
1169
+ composePath(),
1170
+ "exec",
1171
+ "-T",
1172
+ "openwa-api",
1173
+ "cat",
1174
+ "/app/data/.api-key"
1175
+ ],
1082
1176
  { encoding: "utf-8" }
1083
1177
  );
1084
1178
  const openwaApiKey = keyResult.stdout?.trim();
1085
1179
  if (keyResult.status !== 0 || !openwaApiKey) {
1086
1180
  const errorMsg = keyResult.stderr?.trim() || "Could not read API key from container.";
1087
- throw new Error(`${errorMsg} Check logs with: docker compose -f ${composePath()} logs openwa-api`);
1181
+ throw new Error(
1182
+ `${errorMsg} Check logs with: docker compose -f ${composePath()} logs openwa-api`
1183
+ );
1088
1184
  }
1089
- saveSecrets({ GEMINI_API_KEY: geminiKey.trim(), OPENWA_API_KEY: openwaApiKey });
1185
+ saveSecrets({
1186
+ GEMINI_API_KEY: geminiKey.trim(),
1187
+ OPENWA_API_KEY: openwaApiKey
1188
+ });
1090
1189
  saveConfig({ ...loadConfig(), openwaApiKey, recipients: [{ chatId }] });
1091
1190
  console.warn("Creating WhatsApp session...");
1092
- const sessionRes = await fetchWithTimeout(`${baseUrl}/api/sessions`, {
1093
- method: "POST",
1094
- headers: {
1095
- "X-API-Key": openwaApiKey,
1096
- "Content-Type": "application/json"
1191
+ const sessionRes = await fetchWithTimeout(
1192
+ `${baseUrl}/api/sessions`,
1193
+ {
1194
+ method: "POST",
1195
+ headers: {
1196
+ "X-API-Key": openwaApiKey,
1197
+ "Content-Type": "application/json"
1198
+ },
1199
+ body: JSON.stringify({ name: SESSION_NAME })
1097
1200
  },
1098
- body: JSON.stringify({ name: "waify" })
1099
- }, 1e4);
1201
+ 1e4
1202
+ );
1100
1203
  let sessionId;
1101
1204
  if (sessionRes.status === 409) {
1102
- const listRes = await fetchWithTimeout(`${baseUrl}/api/sessions`, {
1103
- headers: { "X-API-Key": openwaApiKey }
1104
- }, 1e4);
1205
+ const listRes = await fetchWithTimeout(
1206
+ `${baseUrl}/api/sessions`,
1207
+ {
1208
+ headers: { "X-API-Key": openwaApiKey }
1209
+ },
1210
+ 1e4
1211
+ );
1105
1212
  if (!listRes.ok) {
1106
- throw new Error(`Failed to list sessions: ${listRes.status} ${listRes.statusText}`);
1213
+ throw new Error(
1214
+ `Failed to list sessions: ${listRes.status} ${listRes.statusText}`
1215
+ );
1107
1216
  }
1108
1217
  const sessions = SessionListSchema.parse(await listRes.json());
1109
- const existing = sessions.find((s) => s.name === "waify");
1218
+ const existing = sessions.find((s) => s.name === SESSION_NAME);
1110
1219
  if (!existing?.id) {
1111
- throw new Error('Session "waify" already exists but could not be retrieved');
1220
+ throw new Error(
1221
+ 'Session "waify" already exists but could not be retrieved'
1222
+ );
1112
1223
  }
1113
1224
  sessionId = existing.id;
1114
1225
  } else if (!sessionRes.ok) {
1115
- throw new Error(`Failed to create session: ${sessionRes.status} ${sessionRes.statusText}`);
1226
+ throw new Error(
1227
+ `Failed to create session: ${sessionRes.status} ${sessionRes.statusText}`
1228
+ );
1116
1229
  } else {
1117
- const sessionData = SessionResponseSchema.parse(await sessionRes.json());
1118
- sessionId = sessionData.id ?? sessionData.name ?? "waify";
1230
+ const sessionData = SessionResponseSchema.parse(
1231
+ await sessionRes.json()
1232
+ );
1233
+ sessionId = sessionData.id ?? sessionData.name ?? SESSION_NAME;
1119
1234
  }
1120
1235
  spawnSync(
1121
1236
  "docker",
@@ -1128,25 +1243,37 @@ var registerSetup = (program2) => {
1128
1243
  "openwa-api",
1129
1244
  "sh",
1130
1245
  "-c",
1131
- "rm -f /app/data/sessions/session-waify/Singleton*"
1246
+ `rm -f /app/data/sessions/session-${SESSION_NAME}/Singleton*`
1132
1247
  ],
1133
1248
  { encoding: "utf-8" }
1134
1249
  );
1135
1250
  console.warn("Starting WhatsApp engine...");
1136
- const startRes = await fetchWithTimeout(`${baseUrl}/api/sessions/${sessionId}/start`, {
1137
- method: "POST",
1138
- headers: { "X-API-Key": openwaApiKey }
1139
- }, 1e4);
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
+ );
1140
1259
  if (!startRes.ok && startRes.status !== 400) {
1141
- throw new Error(`Failed to start session: ${startRes.status} ${startRes.statusText}`);
1260
+ throw new Error(
1261
+ `Failed to start session: ${startRes.status} ${startRes.statusText}`
1262
+ );
1142
1263
  }
1143
- console.warn("Waiting for QR code...");
1264
+ const qrSpinner = createSpinner("Waiting for QR code (Chromium is starting)...");
1144
1265
  let qrCode;
1145
- for (let attempt = 0; attempt < 30; attempt++) {
1266
+ const qrStart = Date.now();
1267
+ for (let attempt = 0; attempt < 150; attempt++) {
1268
+ const elapsed = Math.round((Date.now() - qrStart) / 1e3);
1269
+ qrSpinner.update(`Waiting for QR code... (${elapsed}s / 5 min)`);
1146
1270
  try {
1147
- const qrRes = await fetchWithTimeout(`${baseUrl}/api/sessions/${sessionId}/qr`, {
1148
- headers: { "X-API-Key": openwaApiKey }
1149
- });
1271
+ const qrRes = await fetchWithTimeout(
1272
+ `${baseUrl}/api/sessions/${sessionId}/qr`,
1273
+ {
1274
+ headers: { "X-API-Key": openwaApiKey }
1275
+ }
1276
+ );
1150
1277
  if (qrRes.ok) {
1151
1278
  const qrData = QrResponseSchema.parse(await qrRes.json());
1152
1279
  if (qrData.qrCode) {
@@ -1158,23 +1285,36 @@ var registerSetup = (program2) => {
1158
1285
  }
1159
1286
  await wait(2e3);
1160
1287
  }
1161
- console.warn("\n\u{1F4F1} Scan the QR code with WhatsApp to link your device:");
1288
+ qrSpinner.stop();
1289
+ console.warn(
1290
+ "\n\u{1F4F1} Scan the QR code below with WhatsApp to link your device:"
1291
+ );
1162
1292
  console.warn(" Settings \u2192 Linked Devices \u2192 Link a Device\n");
1163
1293
  if (qrCode) {
1164
- await renderQr(qrCode);
1165
- console.warn("\n (QR expires in ~20s \u2014 re-run setup if it expires before you scan)");
1294
+ await renderQrInTerminal(qrCode);
1295
+ console.warn(
1296
+ "\n (QR expires in ~20s \u2014 re-run setup if it expires before you scan)"
1297
+ );
1166
1298
  } else {
1167
- console.warn(` QR not yet ready. Check: ${baseUrl}/api/sessions/${sessionId}/qr`);
1168
- console.warn(` (Add header: X-API-Key: ${openwaApiKey})`);
1299
+ console.warn(
1300
+ " QR code was not ready in time. Re-run `waify setup` to try again."
1301
+ );
1169
1302
  }
1170
- console.warn(" Waiting up to 2 minutes for you to scan...\n");
1303
+ const connectSpinner = createSpinner(
1304
+ "Waiting for you to scan the QR code..."
1305
+ );
1171
1306
  let connected = false;
1172
1307
  for (let attempt = 0; attempt < 60; attempt++) {
1173
1308
  try {
1174
- const statusRes = await fetchWithTimeout(`${baseUrl}/api/sessions/${sessionId}`, {
1175
- headers: { "X-API-Key": openwaApiKey }
1176
- });
1177
- const parsed = StatusResponseSchema.safeParse(await statusRes.json());
1309
+ const statusRes = await fetchWithTimeout(
1310
+ `${baseUrl}/api/sessions/${sessionId}`,
1311
+ {
1312
+ headers: { "X-API-Key": openwaApiKey }
1313
+ }
1314
+ );
1315
+ const parsed = StatusResponseSchema.safeParse(
1316
+ await statusRes.json()
1317
+ );
1178
1318
  if (!parsed.success) continue;
1179
1319
  if (parsed.data.status === "ready") {
1180
1320
  connected = true;
@@ -1185,11 +1325,13 @@ var registerSetup = (program2) => {
1185
1325
  await wait(2e3);
1186
1326
  }
1187
1327
  if (!connected) {
1188
- console.error("WhatsApp did not connect within 2 minutes. Please re-run `waify setup` to try again.");
1328
+ connectSpinner.fail(
1329
+ "WhatsApp did not connect within 2 minutes. Please re-run `waify setup` to try again."
1330
+ );
1189
1331
  process.exitCode = 1;
1190
1332
  return;
1191
1333
  }
1192
- console.warn("\u2713 WhatsApp connected!");
1334
+ connectSpinner.succeed("WhatsApp connected!");
1193
1335
  saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
1194
1336
  if (!existsSync6(promptPath())) {
1195
1337
  savePrompt(defaultPrompt);
@@ -1197,7 +1339,9 @@ var registerSetup = (program2) => {
1197
1339
  if (!existsSync6(scheduleJsonPath())) {
1198
1340
  saveSchedule(defaultSchedule);
1199
1341
  }
1200
- console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
1342
+ console.warn(
1343
+ "\n\u2713 All done! Run `waify send` to send your first message."
1344
+ );
1201
1345
  } catch (err) {
1202
1346
  console.error(err instanceof Error ? err.message : String(err));
1203
1347
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckasoft/waify",
3
- "version": "0.3.10",
3
+ "version": "0.4.0",
4
4
  "description": "AI-powered daily message sender for WhatsApp, powered by OpenWA",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -41,6 +41,8 @@
41
41
  "ink-select-input": "^6.2.0",
42
42
  "ink-spinner": "^5.0.0",
43
43
  "ink-text-input": "^6.0.0",
44
+ "jsqr": "^1.4.0",
45
+ "pngjs": "^7.0.0",
44
46
  "qrcode-terminal": "^0.12.0",
45
47
  "react": "^19.2.6",
46
48
  "zod": "^3.24.0"
@@ -49,6 +51,7 @@
49
51
  "@semantic-release/changelog": "^6.0.3",
50
52
  "@semantic-release/git": "^10.0.1",
51
53
  "@types/node": "^22.0.0",
54
+ "@types/pngjs": "^6.0.5",
52
55
  "@types/qrcode-terminal": "^0.12.2",
53
56
  "@types/react": "^19.2.15",
54
57
  "semantic-release": "^25.0.3",