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 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 anything (JSON CSV ↔ Excel)
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 -f xlsx # JSON → Excel
163
- npx lacspace-leads convert data.csv -o data.json # CSV JSON
164
- npx lacspace-leads convert sheet.xlsx -f csv # Excel CSV
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
- Programmatically: `convertFile(input, { format, out, sheetName })`, or `readRows(file)` + `serializeRows(rows, format)`.
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
- ### Examples
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
- # Just the essentials for outreach, as CSV
176
- npx lacspace-leads --type "dental clinic" --city Pokhara --fields name,phone,website -f csv -n 40
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
- # Fast name + URL sweep, no per-listing opening
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.0",
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": {