sproutkit-code 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -80729,39 +80729,6 @@ var require_lead_search = __commonJS({
80729
80729
  exports2.searchAndSaveLeads = searchAndSaveLeads;
80730
80730
  var db_1 = require_db();
80731
80731
  var places_1 = require_places();
80732
- var llm_1 = require_llm();
80733
- async function hasRealWebsite(cand, anthropicKey) {
80734
- const prompt = `Search the web to determine whether this specific local business has its OWN real, live website (a site they control \u2014 e.g. a .com/.co.uk, Wix, Squarespace, or self-hosted site).
80735
-
80736
- Business: "${cand.name}"
80737
- Address: ${cand.address}${cand.city ? `, ${cand.city}` : ""}
80738
-
80739
- Rules for your judgement:
80740
- - A Facebook, Instagram, or other social-media page is NOT a website.
80741
- - A directory listing (Yelp, Yell, TripAdvisor, Google profile, JustEat, Deliveroo, Treatwell, Fresha, etc.) is NOT their own website.
80742
- - A parked, for-sale, or dead domain with no real content is NOT a live website.
80743
- - Only answer true if you find a genuine website that clearly belongs to THIS business.
80744
- - If you are unsure, or can't confirm, answer false.
80745
-
80746
- Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|null}`;
80747
- try {
80748
- const res = await (0, llm_1.generateText)({
80749
- apiKey: anthropicKey,
80750
- prompt,
80751
- allowWebSearch: true,
80752
- maxTokens: 1024,
80753
- timeoutMs: 9e4
80754
- });
80755
- const raw = res.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
80756
- const match = raw.match(/\{[\s\S]*\}/);
80757
- if (!match)
80758
- return false;
80759
- const parsed = JSON.parse(match[0]);
80760
- return parsed.hasWebsite === true;
80761
- } catch {
80762
- return false;
80763
- }
80764
- }
80765
80732
  async function searchAndSaveLeads(args) {
80766
80733
  const db2 = (0, db_1.getDb)();
80767
80734
  const results = await (0, places_1.searchPlaces)({
@@ -80773,20 +80740,10 @@ Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|
80773
80740
  radiusMeters: args.radiusMeters
80774
80741
  });
80775
80742
  let saved = 0;
80776
- let verifiedOut = 0;
80777
80743
  for (const r of results) {
80778
80744
  const before = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
80779
80745
  if (before.length > 0)
80780
80746
  continue;
80781
- if (args.verifyNoWebsite) {
80782
- args.onProgress?.(`Checking ${r.name} has no website\u2026`);
80783
- const real = await hasRealWebsite({ name: r.name, address: r.address, city: args.city }, args.anthropicKey);
80784
- if (real) {
80785
- verifiedOut++;
80786
- args.onProgress?.(`${r.name} already has a website \u2014 skipping.`);
80787
- continue;
80788
- }
80789
- }
80790
80747
  db2.run(`INSERT INTO leads
80791
80748
  (place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status)
80792
80749
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered')`, [
@@ -80807,7 +80764,7 @@ Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|
80807
80764
  ]);
80808
80765
  saved++;
80809
80766
  }
80810
- return { found: results.length, saved, verifiedOut };
80767
+ return { found: results.length, saved };
80811
80768
  }
80812
80769
  }
80813
80770
  });
@@ -81146,12 +81103,7 @@ Interested?`;
81146
81103
  types: categories,
81147
81104
  maxResults: Math.min(Number(input.maxResults ?? 25), 100),
81148
81105
  center: place ? { lat: place.lat, lng: place.lng } : void 0,
81149
- radiusMeters: Number(input.radiusMeters ?? 3e3),
81150
- // Web-search each candidate to confirm no real website exists before
81151
- // saving. Skippable via verify:false. Fails open on any error.
81152
- verifyNoWebsite: input.verify !== false,
81153
- anthropicKey: getDecryptedSetting("anthropic_key"),
81154
- onProgress: (msg) => ctx.emitProgress?.({ channel: "leads:searching", payload: { msg } })
81106
+ radiusMeters: Number(input.radiusMeters ?? 3e3)
81155
81107
  });
81156
81108
  ctx.emitProgress?.({
81157
81109
  channel: "leads:changed",
@@ -82320,7 +82272,7 @@ var require_package = __commonJS({
82320
82272
  "package.json"(exports2, module2) {
82321
82273
  module2.exports = {
82322
82274
  name: "sproutkit-code",
82323
- version: "0.4.0",
82275
+ version: "0.5.0",
82324
82276
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
82325
82277
  bin: {
82326
82278
  sproutkit: "./dist/index.js"
@@ -82576,6 +82528,77 @@ var Status = class {
82576
82528
  else say(` ${mark} ${label}`);
82577
82529
  }
82578
82530
  };
82531
+ var STATE_MARK = {
82532
+ queued: () => c.dim("\xB7"),
82533
+ building: () => c.cyan(""),
82534
+ // spinner frame filled in at render time
82535
+ built: () => c.green("\u2713"),
82536
+ failed: () => c.red("\u2717"),
82537
+ skipped: () => c.dim("\u2013")
82538
+ };
82539
+ var StatusBoard = class {
82540
+ rows = [];
82541
+ frame = 0;
82542
+ timer = null;
82543
+ lastLineCount = 0;
82544
+ tty = process.stdout.isTTY && useColor;
82545
+ // Non-TTY: remember the last printed signature per row so we only log changes.
82546
+ lastSig = /* @__PURE__ */ new Map();
82547
+ header = "";
82548
+ start(header, rows) {
82549
+ this.header = header;
82550
+ this.rows = rows;
82551
+ if (!this.tty) {
82552
+ say(c.cyan(` \u27F3 ${header}`));
82553
+ this.rows.forEach((r) => this.logRowIfChanged(r));
82554
+ return;
82555
+ }
82556
+ say(c.cyan(` \u27F3 ${header}`));
82557
+ this.render();
82558
+ this.timer = setInterval(() => {
82559
+ this.frame = (this.frame + 1) % FRAMES.length;
82560
+ this.render();
82561
+ }, 90);
82562
+ }
82563
+ update(row) {
82564
+ const i = this.rows.findIndex((r) => r.key === row.key);
82565
+ if (i >= 0) this.rows[i] = row;
82566
+ else this.rows.push(row);
82567
+ if (!this.tty) this.logRowIfChanged(row);
82568
+ else this.render();
82569
+ }
82570
+ // Replace the whole set at once (e.g. from a job snapshot).
82571
+ set(rows) {
82572
+ this.rows = rows;
82573
+ if (!this.tty) rows.forEach((r) => this.logRowIfChanged(r));
82574
+ else this.render();
82575
+ }
82576
+ line(r) {
82577
+ const mark = r.state === "building" ? c.cyan(FRAMES[this.frame]) : STATE_MARK[r.state]();
82578
+ const detail = r.detail ? c.dim(` \xB7 ${r.detail}`) : "";
82579
+ return ` ${mark} ${r.label}${detail}`;
82580
+ }
82581
+ logRowIfChanged(r) {
82582
+ const sig = `${r.state}:${r.detail ?? ""}`;
82583
+ if (this.lastSig.get(r.key) === sig) return;
82584
+ this.lastSig.set(r.key, sig);
82585
+ const mark = r.state === "built" ? "\u2713" : r.state === "failed" ? "\u2717" : r.state === "skipped" ? "\u2013" : "\u2026";
82586
+ say(` ${mark} ${r.label}${r.detail ? ` \xB7 ${r.detail}` : ""}`);
82587
+ }
82588
+ render() {
82589
+ if (this.lastLineCount > 0) process.stdout.write(`\x1B[${this.lastLineCount}A`);
82590
+ const out = this.rows.map((r) => `\x1B[2K${this.line(r)}`).join("\n");
82591
+ process.stdout.write(out + "\n");
82592
+ this.lastLineCount = this.rows.length;
82593
+ }
82594
+ stop() {
82595
+ if (this.timer) {
82596
+ clearInterval(this.timer);
82597
+ this.timer = null;
82598
+ }
82599
+ if (this.tty) this.render();
82600
+ }
82601
+ };
82579
82602
 
82580
82603
  // src/skill-install.ts
82581
82604
  var import_fs = __toESM(require("fs"));
@@ -83370,6 +83393,70 @@ async function cmdOutreach(argv) {
83370
83393
  if (result.noPhone) say(c.dim(` ${result.noPhone} had no phone number and were skipped for messaging.`));
83371
83394
  say();
83372
83395
  }
83396
+ async function cmdBuildAll(argv) {
83397
+ const { flags } = parseArgs(argv);
83398
+ const json = !!flags.json;
83399
+ const ids = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
83400
+ const leadIds = ids.length ? ids : (await runTool("list_leads", { limit: 100 }, makeCtx({ json }))).filter((l) => !l.site_url).map((l) => l.id);
83401
+ if (leadIds.length === 0) {
83402
+ const msg = "No unbuilt leads to build. Find some first, or pass --ids.";
83403
+ json ? emitJson({ ok: false, error: msg }) : say(c.amber(" " + msg));
83404
+ return;
83405
+ }
83406
+ const anthropicKey = getSetting("anthropic_key");
83407
+ const vercelToken = getSetting("vercel_token");
83408
+ if (!vercelToken) {
83409
+ const msg = "Vercel token not set. Run 'sproutkit setup'.";
83410
+ json ? emitJson({ ok: false, error: msg }) : say(c.red(" " + msg));
83411
+ process.exitCode = 1;
83412
+ return;
83413
+ }
83414
+ if (getBackend() === "api" && !anthropicKey) {
83415
+ const msg = "Anthropic API key not set. Run 'sproutkit setup'.";
83416
+ json ? emitJson({ ok: false, error: msg }) : say(c.red(" " + msg));
83417
+ process.exitCode = 1;
83418
+ return;
83419
+ }
83420
+ const concurrency = Math.min(Math.max(Number(flags.concurrency ?? 3), 1), 4);
83421
+ const board = json ? null : new StatusBoard();
83422
+ const stateToRow = (l) => ({
83423
+ key: l.leadId,
83424
+ label: `#${l.leadId} ${l.name}`,
83425
+ state: l.status,
83426
+ detail: l.status === "built" ? l.siteUrl : l.status === "failed" ? l.error : l.message
83427
+ });
83428
+ await new Promise((resolve) => {
83429
+ const emit = (channel, payload) => {
83430
+ const snap2 = payload;
83431
+ if (channel === "auto:progress" && board) board.set(snap2.leads.map(stateToRow));
83432
+ if (channel === "auto:done") {
83433
+ if (board) {
83434
+ board.set(snap2.leads.map(stateToRow));
83435
+ board.stop();
83436
+ }
83437
+ if (json) emitJson(snap2);
83438
+ else {
83439
+ say();
83440
+ say(
83441
+ c.green(` Done \u2014 ${snap2.completed} built`) + (snap2.failed ? c.red(`, ${snap2.failed} failed`) : "") + c.dim(` (concurrency ${snap2.concurrency}).`)
83442
+ );
83443
+ say();
83444
+ }
83445
+ autoSync();
83446
+ resolve();
83447
+ }
83448
+ };
83449
+ const snap = startAutoJob({
83450
+ leadIds,
83451
+ concurrency,
83452
+ anthropicKey: anthropicKey ?? "",
83453
+ vercelToken,
83454
+ placesKey: getSetting("places_key"),
83455
+ emit
83456
+ });
83457
+ if (board) board.start(`Building ${leadIds.length} sites (up to ${concurrency} at once)`, snap.leads.map(stateToRow));
83458
+ });
83459
+ }
83373
83460
 
83374
83461
  // src/index.ts
83375
83462
  process.env.SPROUTKIT_HEADLESS = "1";
@@ -83405,7 +83492,7 @@ async function main() {
83405
83492
  closeTty();
83406
83493
  return;
83407
83494
  }
83408
- if (arg === "find" || arg === "list" || arg === "build" || arg === "outreach") {
83495
+ if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "outreach") {
83409
83496
  const subArgs = process.argv.slice(3);
83410
83497
  const lic2 = await check();
83411
83498
  if (lic2.status !== "active") {
@@ -83419,6 +83506,7 @@ async function main() {
83419
83506
  if (arg === "find") await cmdFind(subArgs);
83420
83507
  else if (arg === "list") await cmdList(subArgs);
83421
83508
  else if (arg === "build") await cmdBuild(subArgs);
83509
+ else if (arg === "build-all") await cmdBuildAll(subArgs);
83422
83510
  else await cmdOutreach(subArgs);
83423
83511
  closeTty();
83424
83512
  return;
@@ -83432,6 +83520,7 @@ async function main() {
83432
83520
  say(` ${c.bold('sproutkit find "<area>"')} find leads (--categories "barber shop,cafe")`);
83433
83521
  say(` ${c.bold("sproutkit list")} list leads (--status, --city, --json)`);
83434
83522
  say(` ${c.bold("sproutkit build <id>")} build a website (--instructions, --cms)`);
83523
+ say(` ${c.bold("sproutkit build-all")} build many at once (--ids, --concurrency 3)`);
83435
83524
  say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
83436
83525
  say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
83437
83526
  say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
@@ -22,10 +22,8 @@ you can parse results reliably, then summarise them for the user in prose.
22
22
 
23
23
  - **Find leads** — `sproutkit find "<area>" --categories "<type>,<type>" --json`
24
24
  - Searches Google Places for businesses of the given categories in the area,
25
- filters out any that already have a website, and saves the rest as leads.
26
- - It also web-searches each candidate to confirm it has no real website before
27
- saving — so the results are already verified. Social pages and directory
28
- listings don't count as a website; those businesses are kept as leads.
25
+ filters out any that already have a website (per Google's data), and saves
26
+ the rest as leads.
29
27
  - Example: `sproutkit find "Shoreditch, London" --categories "barber shop,cafe" --json`
30
28
  - Optional: `--max 25`, `--radius 3000`.
31
29
 
@@ -40,6 +38,16 @@ you can parse results reliably, then summarise them for the user in prose.
40
38
  - Returns the live `url`. If the lead already has a site, it returns
41
39
  `{"ok":false,...}` — tell the user and offer to regenerate.
42
40
 
41
+ - **Build MANY sites at once** — `sproutkit build-all --json` (optional `--ids 3,5,9`, `--concurrency 3`)
42
+ - Builds several sites in parallel using its own concurrency pool and shows a
43
+ live per-site board. With no `--ids`, it builds every lead that has no site.
44
+ - **Use this for any bulk request** ("build them all", "build the best 5",
45
+ "build sites for leads 1-10"). Do NOT spawn your own subagents to run
46
+ `sproutkit build` many times in parallel — `build-all` owns the
47
+ parallelism, keeps it within the user's subscription rate limits, and
48
+ renders the progress. One `sproutkit build-all` call is correct; N parallel
49
+ `sproutkit build` calls via subagents is not.
50
+
43
51
  - **Prepare outreach** — `sproutkit outreach --json` (optional `--ids 3,5,9`)
44
52
  - Fills the message template, moves the leads to Contacted, and returns a
45
53
  link per lead to open in WhatsApp/SMS. It does NOT auto-send — give the user
@@ -54,7 +62,7 @@ Chain commands to fulfil what the user asked, one step at a time:
54
62
  - "Find cafes in Peckham and build the best two" → find, then `sproutkit list --json` to see what came back, then `sproutkit build <id> --json` for the two highest-scoring.
55
63
  - "Build a site for The Oak Café" → `sproutkit list --json`, find the matching lead's id, then `sproutkit build <id> --json`.
56
64
  - "Message everyone I've built sites for" → `sproutkit outreach --json`, then list the links for the user to open.
57
- - "Find 20 cafes in Bristol, build them all, and prep outreach" → find, list to get ids, build each, then outreach.
65
+ - "Find 20 cafes in Bristol, build them all, and prep outreach" → find, then `sproutkit build-all --json` (NOT twenty separate build calls), then `sproutkit outreach --json`.
58
66
 
59
67
  Rules:
60
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.4.0",
3
+ "version": "0.5.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"