sproutkit-code 0.5.0 → 0.6.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/index.js +165 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -80585,6 +80585,7 @@ var require_places = __commonJS({
80585
80585
  Object.defineProperty(exports2, "__esModule", { value: true });
80586
80586
  exports2.PLACES_COST_PER_REQUEST_GBP = void 0;
80587
80587
  exports2.scoreLead = scoreLead;
80588
+ exports2.searchPlacesStream = searchPlacesStream;
80588
80589
  exports2.searchPlaces = searchPlaces;
80589
80590
  exports2.geocodeForward = geocodeForward;
80590
80591
  exports2.geocodeReverse = geocodeReverse;
@@ -80617,33 +80618,90 @@ var require_places = __commonJS({
80617
80618
  return Math.max(0, Math.round(score));
80618
80619
  }
80619
80620
  var COST_PER_REQUEST_GBP = 0.025;
80620
- async function searchPlaces(args) {
80621
- const results = [];
80622
- const seen = /* @__PURE__ */ new Set();
80623
- for (const type of args.types) {
80624
- if (results.length >= args.maxResults)
80625
- break;
80626
- const body = {
80627
- textQuery: `${type} in ${args.city}`,
80628
- maxResultCount: 20
80621
+ async function searchOneType(args) {
80622
+ const body = {
80623
+ textQuery: `${args.type} in ${args.city}`,
80624
+ maxResultCount: 20
80625
+ };
80626
+ if (args.center && args.radiusMeters) {
80627
+ body.locationBias = {
80628
+ circle: {
80629
+ center: { latitude: args.center.lat, longitude: args.center.lng },
80630
+ radius: args.radiusMeters
80631
+ }
80629
80632
  };
80630
- if (args.center && args.radiusMeters) {
80631
- body.locationBias = {
80632
- circle: {
80633
- center: { latitude: args.center.lat, longitude: args.center.lng },
80634
- radius: args.radiusMeters
80635
- }
80636
- };
80637
- }
80633
+ }
80634
+ try {
80638
80635
  const r = await axios_1.default.post("https://places.googleapis.com/v1/places:searchText", body, {
80639
80636
  headers: {
80640
80637
  "X-Goog-Api-Key": args.apiKey,
80641
80638
  "X-Goog-FieldMask": "places.id,places.displayName,places.formattedAddress,places.nationalPhoneNumber,places.websiteUri,places.rating,places.userRatingCount,places.location,places.priceLevel,places.businessStatus,places.regularOpeningHours"
80642
80639
  }
80643
80640
  });
80644
- for (const p of r.data.places ?? []) {
80645
- if (results.length >= args.maxResults)
80646
- break;
80641
+ return { type: args.type, places: r.data.places ?? [] };
80642
+ } catch {
80643
+ return { type: args.type, places: [] };
80644
+ }
80645
+ }
80646
+ function processRawPlaces(type, places, center, radiusMeters) {
80647
+ const out = [];
80648
+ for (const p of places) {
80649
+ if (p.websiteUri)
80650
+ continue;
80651
+ if (center && radiusMeters && p.location) {
80652
+ const d = haversineMeters(center.lat, center.lng, p.location.latitude, p.location.longitude);
80653
+ if (d > radiusMeters)
80654
+ continue;
80655
+ }
80656
+ const phone = p.nationalPhoneNumber ?? "";
80657
+ const rating = p.rating ?? null;
80658
+ const review_count = p.userRatingCount ?? null;
80659
+ const price_level = p.priceLevel ?? null;
80660
+ const business_status = p.businessStatus ?? null;
80661
+ const has_hours = !!p.regularOpeningHours;
80662
+ out.push({
80663
+ place_id: p.id,
80664
+ name: p.displayName?.text ?? "Unknown",
80665
+ type,
80666
+ address: p.formattedAddress ?? "",
80667
+ phone,
80668
+ website: p.websiteUri ?? null,
80669
+ rating,
80670
+ review_count,
80671
+ lat: p.location?.latitude ?? null,
80672
+ lng: p.location?.longitude ?? null,
80673
+ price_level,
80674
+ business_status,
80675
+ has_hours,
80676
+ score: scoreLead({ rating, review_count, price_level, phone, has_hours, business_status })
80677
+ });
80678
+ }
80679
+ return out;
80680
+ }
80681
+ async function searchPlacesStream(args, onBatch) {
80682
+ await Promise.all(args.types.map(async (type) => {
80683
+ const { places } = await searchOneType({
80684
+ apiKey: args.apiKey,
80685
+ city: args.city,
80686
+ type,
80687
+ center: args.center,
80688
+ radiusMeters: args.radiusMeters
80689
+ });
80690
+ await onBatch(processRawPlaces(type, places, args.center, args.radiusMeters));
80691
+ }));
80692
+ }
80693
+ async function searchPlaces(args) {
80694
+ const batches = await Promise.all(args.types.map((type) => searchOneType({
80695
+ apiKey: args.apiKey,
80696
+ city: args.city,
80697
+ type,
80698
+ center: args.center,
80699
+ radiusMeters: args.radiusMeters
80700
+ })));
80701
+ const results = [];
80702
+ const seen = /* @__PURE__ */ new Set();
80703
+ for (const { type, places } of batches) {
80704
+ for (const p of places) {
80647
80705
  if (seen.has(p.id))
80648
80706
  continue;
80649
80707
  seen.add(p.id);
@@ -80687,7 +80745,7 @@ var require_places = __commonJS({
80687
80745
  }
80688
80746
  }
80689
80747
  results.sort((a, b) => b.score - a.score);
80690
- return results;
80748
+ return results.slice(0, args.maxResults);
80691
80749
  }
80692
80750
  function haversineMeters(lat1, lon1, lat2, lon2) {
80693
80751
  const R = 6371e3;
@@ -80726,9 +80784,58 @@ var require_lead_search = __commonJS({
80726
80784
  "../desktop/dist/main/lib/lead-search.js"(exports2) {
80727
80785
  "use strict";
80728
80786
  Object.defineProperty(exports2, "__esModule", { value: true });
80787
+ exports2.searchAndSaveLeadsStream = searchAndSaveLeadsStream;
80729
80788
  exports2.searchAndSaveLeads = searchAndSaveLeads;
80730
80789
  var db_1 = require_db();
80731
80790
  var places_1 = require_places();
80791
+ async function searchAndSaveLeadsStream(args, onLead) {
80792
+ const db2 = (0, db_1.getDb)();
80793
+ const cap = args.maxResults ?? 50;
80794
+ const seen = /* @__PURE__ */ new Set();
80795
+ let saved = 0;
80796
+ let found = 0;
80797
+ await (0, places_1.searchPlacesStream)({
80798
+ apiKey: args.placesKey,
80799
+ city: args.city,
80800
+ types: args.types,
80801
+ center: args.center,
80802
+ radiusMeters: args.radiusMeters
80803
+ }, (batch) => {
80804
+ for (const r of batch) {
80805
+ found++;
80806
+ if (saved >= cap)
80807
+ continue;
80808
+ if (seen.has(r.place_id))
80809
+ continue;
80810
+ seen.add(r.place_id);
80811
+ const existing = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
80812
+ if (existing.length > 0)
80813
+ continue;
80814
+ db2.run(`INSERT INTO leads
80815
+ (place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status)
80816
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered')`, [
80817
+ r.place_id,
80818
+ r.name,
80819
+ r.type,
80820
+ args.city,
80821
+ r.address,
80822
+ r.phone,
80823
+ r.website,
80824
+ r.rating,
80825
+ r.review_count,
80826
+ r.lat,
80827
+ r.lng,
80828
+ r.score,
80829
+ r.price_level,
80830
+ r.business_status
80831
+ ]);
80832
+ const row = db2.get("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
80833
+ saved++;
80834
+ onLead({ id: row?.id ?? 0, place_id: r.place_id, name: r.name, type: r.type, city: args.city });
80835
+ }
80836
+ });
80837
+ return { found, saved };
80838
+ }
80732
80839
  async function searchAndSaveLeads(args) {
80733
80840
  const db2 = (0, db_1.getDb)();
80734
80841
  const results = await (0, places_1.searchPlaces)({
@@ -81096,15 +81203,20 @@ Interested?`;
81096
81203
  });
81097
81204
  const matches = await (0, places_1.geocodeForward)(location, 1);
81098
81205
  const place = matches[0];
81099
- const result = await (0, lead_search_1.searchAndSaveLeads)({
81100
- placesKey,
81101
- // searchPlaces uses `city` as the text query, so keep the user's words.
81102
- city: location,
81103
- types: categories,
81104
- maxResults: Math.min(Number(input.maxResults ?? 25), 100),
81105
- center: place ? { lat: place.lat, lng: place.lng } : void 0,
81106
- radiusMeters: Number(input.radiusMeters ?? 3e3)
81107
- });
81206
+ const result = await (0, lead_search_1.searchAndSaveLeadsStream)(
81207
+ {
81208
+ placesKey,
81209
+ // searchPlaces uses `city` as the text query, so keep the user's words.
81210
+ city: location,
81211
+ types: categories,
81212
+ maxResults: Math.min(Number(input.maxResults ?? 25), 100),
81213
+ center: place ? { lat: place.lat, lng: place.lng } : void 0,
81214
+ radiusMeters: Number(input.radiusMeters ?? 3e3)
81215
+ },
81216
+ // Emit each lead the instant it's saved, so the UI/CLI streams results
81217
+ // in rather than waiting for every category to finish.
81218
+ (lead) => ctx.emitProgress?.({ channel: "leads:found", payload: { lead } })
81219
+ );
81108
81220
  ctx.emitProgress?.({
81109
81221
  channel: "leads:changed",
81110
81222
  payload: { reason: "find_leads", ...result }
@@ -82272,7 +82384,7 @@ var require_package = __commonJS({
82272
82384
  "package.json"(exports2, module2) {
82273
82385
  module2.exports = {
82274
82386
  name: "sproutkit-code",
82275
- version: "0.5.0",
82387
+ version: "0.6.0",
82276
82388
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
82277
82389
  bin: {
82278
82390
  sproutkit: "./dist/index.js"
@@ -83234,6 +83346,7 @@ async function runModels() {
83234
83346
  // src/commands.ts
83235
83347
  function makeCtx(opts) {
83236
83348
  const emit = (channel, payload) => {
83349
+ opts.onEvent?.(channel, payload);
83237
83350
  if (opts.json) return;
83238
83351
  const p = payload;
83239
83352
  if (p?.msg && opts.status) opts.status.update(p.msg);
@@ -83277,8 +83390,18 @@ async function cmdFind(argv) {
83277
83390
  return;
83278
83391
  }
83279
83392
  const categories = String(flags.categories ?? "").split(",").map((s) => s.trim()).filter(Boolean);
83280
- const status = json ? void 0 : new Status();
83281
- if (status) status.start(`Finding leads in ${location}`);
83393
+ if (!json) say(c.dim(` Searching ${location}${categories.length ? ` for ${categories.join(", ")}` : ""}\u2026`));
83394
+ let streamed = 0;
83395
+ const onEvent = (channel, payload) => {
83396
+ if (json) return;
83397
+ if (channel === "leads:found") {
83398
+ const lead = payload.lead;
83399
+ if (lead) {
83400
+ streamed++;
83401
+ say(` ${c.green("+")} ${c.bold(lead.name)} ${c.dim(`#${lead.id} \xB7 ${lead.type}`)}`);
83402
+ }
83403
+ }
83404
+ };
83282
83405
  try {
83283
83406
  const result = await runTool(
83284
83407
  "find_leads",
@@ -83288,14 +83411,18 @@ async function cmdFind(argv) {
83288
83411
  maxResults: flags.max ? Number(flags.max) : void 0,
83289
83412
  radiusMeters: flags.radius ? Number(flags.radius) : void 0
83290
83413
  },
83291
- makeCtx({ json, status })
83414
+ makeCtx({ json, onEvent })
83292
83415
  );
83293
- if (status) status.done(true, `Found ${result.found ?? 0}, saved ${result.saved ?? 0} new`);
83294
83416
  autoSync();
83295
- if (json) emitJson(result);
83296
- else if (result.note) say(c.dim(" " + result.note));
83417
+ if (json) {
83418
+ emitJson(result);
83419
+ } else {
83420
+ say();
83421
+ say(c.green(` Saved ${result.saved ?? streamed} new lead${(result.saved ?? streamed) === 1 ? "" : "s"}.`));
83422
+ if (result.note) say(c.dim(" " + result.note));
83423
+ say();
83424
+ }
83297
83425
  } catch (e) {
83298
- if (status) status.done(false, "Find failed");
83299
83426
  json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
83300
83427
  process.exitCode = 1;
83301
83428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Sproutkit in your terminal — find leads, build sites, run outreach, all via chat.",
5
5
  "bin": {
6
6
  "sproutkit": "./dist/index.js"