@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.
- package/dist/cli/index.js +123 -51
- 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
|
|
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
|
-
|
|
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
|
|
311
|
-
import { dirname as
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1034
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|