lacspace-leads 1.3.0 → 1.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.
- package/README.md +71 -10
- package/dist/cli.js +22 -2
- package/dist/lib.cjs +14 -1
- package/dist/lib.d.cts +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,29 +154,90 @@ npx lacspace-leads cafes --city Kathmandu --area "Thamel,Baneshwor,Patan" \
|
|
|
154
154
|
|
|
155
155
|
`--verify-emails` does a DNS **MX lookup** on each email's domain (no message is sent) and adds an `emailStatus` column of `valid` · `no-mx` · `invalid-format`. `--append` reads the existing file back, merges, and de-duplicates with the **`smart`** key (website → phone → name) so even website-less businesses don't pile up on re-runs.
|
|
156
156
|
|
|
157
|
-
## Convert
|
|
157
|
+
## Convert your leads to any format
|
|
158
158
|
|
|
159
|
-
A general converter is built in — it works on any tabular file, not just leads:
|
|
159
|
+
Collected leads as JSON and now need them in Excel? A general converter is built in — it reads and writes **JSON · NDJSON · CSV · Excel** in any direction, and works on *any* tabular file, not just leads:
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
npx lacspace-leads convert leads.json
|
|
163
|
-
npx lacspace-leads convert
|
|
164
|
-
npx lacspace-leads convert
|
|
162
|
+
npx lacspace-leads convert leads.json -f xlsx # JSON → Excel
|
|
163
|
+
npx lacspace-leads convert leads.json -o leads.csv # JSON → CSV (format from the -o extension)
|
|
164
|
+
npx lacspace-leads convert data.csv -o data.json # CSV → JSON
|
|
165
|
+
npx lacspace-leads convert sheet.xlsx -f csv # Excel → CSV
|
|
166
|
+
npx lacspace-leads convert leads.ndjson -f xlsx # NDJSON → Excel
|
|
167
|
+
npx lacspace-leads convert big.csv -f ndjson # CSV → NDJSON (stream-friendly)
|
|
165
168
|
```
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
The output format comes from `-f` (or is inferred from the `-o` filename's extension); if you give neither, it defaults to JSON. Use `--sheet "My Leads"` to name the Excel tab.
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
**What converts to what** — every combination works, both ways:
|
|
173
|
+
|
|
174
|
+
| From \ To | JSON | NDJSON | CSV | Excel |
|
|
175
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
176
|
+
| **JSON** | – | ✓ | ✓ | ✓ |
|
|
177
|
+
| **NDJSON** | ✓ | – | ✓ | ✓ |
|
|
178
|
+
| **CSV** | ✓ | ✓ | – | ✓ |
|
|
179
|
+
| **Excel** | ✓ | ✓ | ✓ | – |
|
|
180
|
+
|
|
181
|
+
**Sample.** `leads.json` in:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
[
|
|
185
|
+
{ "name": "Himalayan Java", "phone": "+9779801234567", "rating": 4.6, "website": "https://himalayanjava.com" },
|
|
186
|
+
{ "name": "Cafe Soma", "phone": "+9779807654321", "rating": 4.4 }
|
|
187
|
+
]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`npx lacspace-leads convert leads.json -o leads.csv` → `leads.csv` out (missing cells become blank; `+` is escaped so spreadsheets don't treat it as a formula):
|
|
191
|
+
|
|
192
|
+
```csv
|
|
193
|
+
name,phone,rating,website
|
|
194
|
+
Himalayan Java,'+9779801234567,4.6,https://himalayanjava.com
|
|
195
|
+
Cafe Soma,'+9779807654321,4.4,
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**In code** — convert a file, or turn any `Lead[]` into a downloadable buffer:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { convertFile, readRows, serializeRows, serialize } from "lacspace-leads";
|
|
202
|
+
|
|
203
|
+
// 1. Convert a file on disk (returns { out, format, count }).
|
|
204
|
+
await convertFile("leads.json", { format: "xlsx", sheetName: "Prospects" });
|
|
205
|
+
|
|
206
|
+
// 2. Read rows from any format, transform, write to another.
|
|
207
|
+
const rows = await readRows("leads.csv"); // → array of objects
|
|
208
|
+
const filtered = rows.filter((r) => Number(r.Rating) >= 4.5);
|
|
209
|
+
const { data, binary } = serializeRows(filtered, "xlsx"); // → bytes for xlsx
|
|
210
|
+
// (write `data` yourself, or in a server route send it as a download)
|
|
211
|
+
|
|
212
|
+
// 3. Serialize Lead[] straight from a search — pick the columns and order.
|
|
213
|
+
const { data: csv } = serialize(leads, "csv", ["name", "phone", "email"]);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Recipes
|
|
170
217
|
|
|
171
218
|
```bash
|
|
172
219
|
# Excel of restaurants in a specific area
|
|
173
220
|
npx lacspace-leads restaurants --city Kathmandu --area Baneshwor -f xlsx
|
|
174
221
|
|
|
175
|
-
#
|
|
176
|
-
npx lacspace-leads
|
|
222
|
+
# Outreach list: name/phone/email/website/address, MX-verified, deliverable only
|
|
223
|
+
npx lacspace-leads "dental clinic" --city Pokhara --preset outreach \
|
|
224
|
+
--verify-emails --has-valid-email --country NP -f csv
|
|
177
225
|
|
|
178
|
-
#
|
|
226
|
+
# Cover a whole city, sorted best-reviewed first, into one Excel file
|
|
227
|
+
npx lacspace-leads cafes --city Kathmandu --area "Thamel,Baneshwor,Patan" \
|
|
228
|
+
--sort reviews --desc -f xlsx
|
|
229
|
+
|
|
230
|
+
# Everything within 1.5 km of a point, nearest first
|
|
231
|
+
npx lacspace-leads salons --near "27.7172,85.3240" --radius 1.5km -f csv
|
|
232
|
+
|
|
233
|
+
# Build a master list you top up daily (accumulate + dedupe)
|
|
234
|
+
npx lacspace-leads gyms --city Lalitpur -o gyms-master.xlsx --append
|
|
235
|
+
|
|
236
|
+
# Fast name + Maps-URL sweep, no per-listing opening
|
|
179
237
|
npx lacspace-leads gyms --city Lalitpur --no-details -n 100
|
|
238
|
+
|
|
239
|
+
# Pipe straight into jq (JSON on stdout, logs on stderr)
|
|
240
|
+
npx lacspace-leads bakeries --city Pokhara -f json -o - -y | jq '.[].phone'
|
|
180
241
|
```
|
|
181
242
|
|
|
182
243
|
## Library
|
package/dist/cli.js
CHANGED
|
@@ -1021,9 +1021,10 @@ import { jsonToXlsx as jsonToXlsx2, xlsxToJson } from "@lacspace/xlsx";
|
|
|
1021
1021
|
function detectFormat(file) {
|
|
1022
1022
|
const ext = extname(file).toLowerCase().replace(/^\./, "");
|
|
1023
1023
|
if (ext === "json") return "json";
|
|
1024
|
+
if (ext === "ndjson" || ext === "jsonl") return "ndjson";
|
|
1024
1025
|
if (ext === "csv" || ext === "tsv") return "csv";
|
|
1025
1026
|
if (ext === "xlsx" || ext === "xls") return "xlsx";
|
|
1026
|
-
throw new Error(`Cannot infer a format from ".${ext}" \u2014 pass an explicit --format (json|csv|xlsx).`);
|
|
1027
|
+
throw new Error(`Cannot infer a format from ".${ext}" \u2014 pass an explicit --format (json|ndjson|csv|xlsx).`);
|
|
1027
1028
|
}
|
|
1028
1029
|
async function readRows(file, format) {
|
|
1029
1030
|
const fmt = format ?? detectFormat(file);
|
|
@@ -1035,6 +1036,15 @@ async function readRows(file, format) {
|
|
|
1035
1036
|
if (fmt === "csv") {
|
|
1036
1037
|
return csvParse(text, { header: true });
|
|
1037
1038
|
}
|
|
1039
|
+
if (fmt === "ndjson") {
|
|
1040
|
+
return text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l, i) => {
|
|
1041
|
+
try {
|
|
1042
|
+
return JSON.parse(l);
|
|
1043
|
+
} catch {
|
|
1044
|
+
throw new Error(`Invalid NDJSON on line ${i + 1}.`);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1038
1048
|
const parsed = JSON.parse(text);
|
|
1039
1049
|
if (Array.isArray(parsed)) return parsed;
|
|
1040
1050
|
if (parsed && typeof parsed === "object") return [parsed];
|
|
@@ -1057,6 +1067,9 @@ function serializeRows(rows, format, opts = {}) {
|
|
|
1057
1067
|
if (format === "json") {
|
|
1058
1068
|
return { data: JSON.stringify(rows, null, 2), binary: false };
|
|
1059
1069
|
}
|
|
1070
|
+
if (format === "ndjson") {
|
|
1071
|
+
return { data: rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""), binary: false };
|
|
1072
|
+
}
|
|
1060
1073
|
const cols = columnsOf(rows);
|
|
1061
1074
|
const flat = rows.map((row) => {
|
|
1062
1075
|
const out = {};
|
|
@@ -1186,7 +1199,7 @@ ${c("bold", c("magenta", "\u25C6 lacspace-leads"))} ${c("dim", "\u2014 free loca
|
|
|
1186
1199
|
|
|
1187
1200
|
${c("bold", "Usage")}
|
|
1188
1201
|
npx lacspace-leads [type] [options]
|
|
1189
|
-
npx lacspace-leads convert <file> [-f json|csv|xlsx] [-o out]
|
|
1202
|
+
npx lacspace-leads convert <file> [-f json|ndjson|csv|xlsx] [-o out]
|
|
1190
1203
|
|
|
1191
1204
|
${c("bold", "Search options")}
|
|
1192
1205
|
-t, --type <text> Business type/keyword. Comma-separate for several,
|
|
@@ -1277,6 +1290,13 @@ async function runConvert(rest) {
|
|
|
1277
1290
|
exit(1);
|
|
1278
1291
|
return;
|
|
1279
1292
|
}
|
|
1293
|
+
if ((rest.includes("-f") || rest.includes("--format")) && !OUTPUT_FORMATS.includes(a.format)) {
|
|
1294
|
+
log(c("red", `
|
|
1295
|
+
\u2717 Unknown format "${a.format}". Use: ${OUTPUT_FORMATS.join(", ")}.
|
|
1296
|
+
`));
|
|
1297
|
+
exit(1);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1280
1300
|
log(`
|
|
1281
1301
|
${c("bold", c("magenta", "\u25C6 lacspace-leads convert"))}
|
|
1282
1302
|
`);
|
package/dist/lib.cjs
CHANGED
|
@@ -1098,9 +1098,10 @@ var import_xlsx2 = require("@lacspace/xlsx");
|
|
|
1098
1098
|
function detectFormat(file) {
|
|
1099
1099
|
const ext = (0, import_node_path.extname)(file).toLowerCase().replace(/^\./, "");
|
|
1100
1100
|
if (ext === "json") return "json";
|
|
1101
|
+
if (ext === "ndjson" || ext === "jsonl") return "ndjson";
|
|
1101
1102
|
if (ext === "csv" || ext === "tsv") return "csv";
|
|
1102
1103
|
if (ext === "xlsx" || ext === "xls") return "xlsx";
|
|
1103
|
-
throw new Error(`Cannot infer a format from ".${ext}" \u2014 pass an explicit --format (json|csv|xlsx).`);
|
|
1104
|
+
throw new Error(`Cannot infer a format from ".${ext}" \u2014 pass an explicit --format (json|ndjson|csv|xlsx).`);
|
|
1104
1105
|
}
|
|
1105
1106
|
async function readRows(file, format) {
|
|
1106
1107
|
const fmt = format ?? detectFormat(file);
|
|
@@ -1112,6 +1113,15 @@ async function readRows(file, format) {
|
|
|
1112
1113
|
if (fmt === "csv") {
|
|
1113
1114
|
return (0, import_csv2.parse)(text, { header: true });
|
|
1114
1115
|
}
|
|
1116
|
+
if (fmt === "ndjson") {
|
|
1117
|
+
return text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l, i) => {
|
|
1118
|
+
try {
|
|
1119
|
+
return JSON.parse(l);
|
|
1120
|
+
} catch {
|
|
1121
|
+
throw new Error(`Invalid NDJSON on line ${i + 1}.`);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1115
1125
|
const parsed = JSON.parse(text);
|
|
1116
1126
|
if (Array.isArray(parsed)) return parsed;
|
|
1117
1127
|
if (parsed && typeof parsed === "object") return [parsed];
|
|
@@ -1134,6 +1144,9 @@ function serializeRows(rows, format, opts = {}) {
|
|
|
1134
1144
|
if (format === "json") {
|
|
1135
1145
|
return { data: JSON.stringify(rows, null, 2), binary: false };
|
|
1136
1146
|
}
|
|
1147
|
+
if (format === "ndjson") {
|
|
1148
|
+
return { data: rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""), binary: false };
|
|
1149
|
+
}
|
|
1137
1150
|
const cols = columnsOf(rows);
|
|
1138
1151
|
const flat = rows.map((row) => {
|
|
1139
1152
|
const out = {};
|
package/dist/lib.d.cts
CHANGED
|
@@ -450,7 +450,7 @@ declare function zoomForRadius(radiusM: number, lat?: number): number;
|
|
|
450
450
|
type DataRow = Record<string, unknown>;
|
|
451
451
|
/** Infer the format from a filename extension. Throws on an unknown extension. */
|
|
452
452
|
declare function detectFormat(file: string): OutputFormat;
|
|
453
|
-
/** Read a JSON / CSV / Excel file into an array of rows. */
|
|
453
|
+
/** Read a JSON / NDJSON / CSV / Excel file into an array of rows. */
|
|
454
454
|
declare function readRows(file: string, format?: OutputFormat): Promise<DataRow[]>;
|
|
455
455
|
/** The union of keys across all rows, in first-seen order — the column set. */
|
|
456
456
|
declare function columnsOf(rows: DataRow[]): string[];
|
package/dist/lib.d.ts
CHANGED
|
@@ -450,7 +450,7 @@ declare function zoomForRadius(radiusM: number, lat?: number): number;
|
|
|
450
450
|
type DataRow = Record<string, unknown>;
|
|
451
451
|
/** Infer the format from a filename extension. Throws on an unknown extension. */
|
|
452
452
|
declare function detectFormat(file: string): OutputFormat;
|
|
453
|
-
/** Read a JSON / CSV / Excel file into an array of rows. */
|
|
453
|
+
/** Read a JSON / NDJSON / CSV / Excel file into an array of rows. */
|
|
454
454
|
declare function readRows(file: string, format?: OutputFormat): Promise<DataRow[]>;
|
|
455
455
|
/** The union of keys across all rows, in first-seen order — the column set. */
|
|
456
456
|
declare function columnsOf(rows: DataRow[]): string[];
|
package/dist/lib.js
CHANGED
|
@@ -1027,9 +1027,10 @@ import { jsonToXlsx as jsonToXlsx2, xlsxToJson } from "@lacspace/xlsx";
|
|
|
1027
1027
|
function detectFormat(file) {
|
|
1028
1028
|
const ext = extname(file).toLowerCase().replace(/^\./, "");
|
|
1029
1029
|
if (ext === "json") return "json";
|
|
1030
|
+
if (ext === "ndjson" || ext === "jsonl") return "ndjson";
|
|
1030
1031
|
if (ext === "csv" || ext === "tsv") return "csv";
|
|
1031
1032
|
if (ext === "xlsx" || ext === "xls") return "xlsx";
|
|
1032
|
-
throw new Error(`Cannot infer a format from ".${ext}" \u2014 pass an explicit --format (json|csv|xlsx).`);
|
|
1033
|
+
throw new Error(`Cannot infer a format from ".${ext}" \u2014 pass an explicit --format (json|ndjson|csv|xlsx).`);
|
|
1033
1034
|
}
|
|
1034
1035
|
async function readRows(file, format) {
|
|
1035
1036
|
const fmt = format ?? detectFormat(file);
|
|
@@ -1041,6 +1042,15 @@ async function readRows(file, format) {
|
|
|
1041
1042
|
if (fmt === "csv") {
|
|
1042
1043
|
return csvParse(text, { header: true });
|
|
1043
1044
|
}
|
|
1045
|
+
if (fmt === "ndjson") {
|
|
1046
|
+
return text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).map((l, i) => {
|
|
1047
|
+
try {
|
|
1048
|
+
return JSON.parse(l);
|
|
1049
|
+
} catch {
|
|
1050
|
+
throw new Error(`Invalid NDJSON on line ${i + 1}.`);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1044
1054
|
const parsed = JSON.parse(text);
|
|
1045
1055
|
if (Array.isArray(parsed)) return parsed;
|
|
1046
1056
|
if (parsed && typeof parsed === "object") return [parsed];
|
|
@@ -1063,6 +1073,9 @@ function serializeRows(rows, format, opts = {}) {
|
|
|
1063
1073
|
if (format === "json") {
|
|
1064
1074
|
return { data: JSON.stringify(rows, null, 2), binary: false };
|
|
1065
1075
|
}
|
|
1076
|
+
if (format === "ndjson") {
|
|
1077
|
+
return { data: rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""), binary: false };
|
|
1078
|
+
}
|
|
1066
1079
|
const cols = columnsOf(rows);
|
|
1067
1080
|
const flat = rows.map((row) => {
|
|
1068
1081
|
const out = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lacspace-leads",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Free, open-source local-business lead finder — name a city/area/business type, or search by coordinates + radius, and it drives a real browser over Google Maps to collect names, phones, websites, ratings, addresses, emails and social links, then exports to JSON, NDJSON, CSV or Excel. Sweep multiple areas at once, verify emails by MX, normalise phones to E.164, accumulate into a master file, and enrich in parallel through an optional proxy. No API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|