sproutkit-code 0.15.0 → 0.16.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 +237 -15
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -35390,7 +35390,7 @@ var require_cross_spawn = __commonJS({
35390
35390
  enoent.hookChildProcess(spawned, parsed);
35391
35391
  return spawned;
35392
35392
  }
35393
- function spawnSync3(command, args, options) {
35393
+ function spawnSync4(command, args, options) {
35394
35394
  const parsed = parse(command, args, options);
35395
35395
  const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
35396
35396
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
@@ -35398,7 +35398,7 @@ var require_cross_spawn = __commonJS({
35398
35398
  }
35399
35399
  module2.exports = spawn2;
35400
35400
  module2.exports.spawn = spawn2;
35401
- module2.exports.sync = spawnSync3;
35401
+ module2.exports.sync = spawnSync4;
35402
35402
  module2.exports._parse = parse;
35403
35403
  module2.exports._enoent = enoent;
35404
35404
  }
@@ -92713,6 +92713,77 @@ var require_email = __commonJS({
92713
92713
  }
92714
92714
  });
92715
92715
 
92716
+ // ../desktop/dist/main/lib/email-harvest.js
92717
+ var require_email_harvest = __commonJS({
92718
+ "../desktop/dist/main/lib/email-harvest.js"(exports2) {
92719
+ "use strict";
92720
+ Object.defineProperty(exports2, "__esModule", { value: true });
92721
+ exports2.harvestEmail = harvestEmail;
92722
+ var llm_1 = require_llm();
92723
+ var BAD_EMAIL_DOMAINS = [
92724
+ "yelp.com",
92725
+ "yell.com",
92726
+ "tripadvisor",
92727
+ "facebook.com",
92728
+ "instagram.com",
92729
+ "google.com",
92730
+ "justeat",
92731
+ "deliveroo",
92732
+ "ubereats",
92733
+ "treatwell",
92734
+ "fresha.com",
92735
+ "booking.com",
92736
+ "example.com",
92737
+ "sentry.io",
92738
+ "wixpress.com",
92739
+ "squarespace.com"
92740
+ ];
92741
+ function plausibleEmail(raw) {
92742
+ if (typeof raw !== "string")
92743
+ return null;
92744
+ const email = raw.trim().toLowerCase();
92745
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email))
92746
+ return null;
92747
+ const domain = email.split("@")[1] ?? "";
92748
+ if (BAD_EMAIL_DOMAINS.some((d) => domain.includes(d)))
92749
+ return null;
92750
+ return email;
92751
+ }
92752
+ async function harvestEmail(args) {
92753
+ const prompt = `Search the web for the contact email address of this specific local business.
92754
+
92755
+ Business: "${args.name}"
92756
+ Address: ${args.address}${args.city ? `, ${args.city}` : ""}
92757
+
92758
+ Rules \u2014 be strict:
92759
+ - The email must clearly belong to THIS business at THIS address/city. A business with the same or similar name in a different town or country is NOT a match \u2014 return null.
92760
+ - Prefer an email on the business's own website or its official social profile.
92761
+ - Do NOT return an email belonging to a directory, aggregator or booking platform (Yelp, Yell, TripAdvisor, JustEat, Deliveroo, Treatwell, Fresha, etc.).
92762
+ - Do NOT guess, construct, or infer an address (e.g. never invent info@<name>.com).
92763
+ - If you cannot confirm a real email for this exact business, return null.
92764
+
92765
+ Respond with ONLY compact JSON, no prose: {"email": string|null}`;
92766
+ try {
92767
+ const res = await (0, llm_1.generateText)({
92768
+ apiKey: args.apiKey,
92769
+ prompt,
92770
+ allowWebSearch: true,
92771
+ maxTokens: 512,
92772
+ timeoutMs: 9e4
92773
+ });
92774
+ const raw = res.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
92775
+ const match = raw.match(/\{[\s\S]*\}/);
92776
+ if (!match)
92777
+ return null;
92778
+ const parsed = JSON.parse(match[0]);
92779
+ return plausibleEmail(parsed.email);
92780
+ } catch {
92781
+ return null;
92782
+ }
92783
+ }
92784
+ }
92785
+ });
92786
+
92716
92787
  // ../desktop/dist/main/lib/chat-tools.js
92717
92788
  var require_chat_tools = __commonJS({
92718
92789
  "../desktop/dist/main/lib/chat-tools.js"(exports2) {
@@ -92726,6 +92797,7 @@ var require_chat_tools = __commonJS({
92726
92797
  var lead_search_1 = require_lead_search();
92727
92798
  var places_1 = require_places();
92728
92799
  var email_1 = require_email();
92800
+ var email_harvest_1 = require_email_harvest();
92729
92801
  exports2.TOOLS = [
92730
92802
  {
92731
92803
  name: "find_leads",
@@ -93160,7 +93232,7 @@ Interested?`;
93160
93232
  }
93161
93233
  case "prepare_outreach": {
93162
93234
  const explicit = input.ids;
93163
- const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, site_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all("SELECT id, name, phone, email, site_url FROM leads WHERE status = 'site_built' AND site_url IS NOT NULL AND site_url != '' ORDER BY score DESC NULLS LAST, id DESC", []);
93235
+ const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, address, city, site_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all("SELECT id, name, phone, email, address, city, site_url FROM leads WHERE status = 'site_built' AND site_url IS NOT NULL AND site_url != '' ORDER BY score DESC NULLS LAST, id DESC", []);
93164
93236
  const eligible = rows.filter((r) => r.site_url);
93165
93237
  if (eligible.length === 0) {
93166
93238
  return {
@@ -93174,9 +93246,27 @@ Interested?`;
93174
93246
  const smtp = (0, email_1.smtpFromSettings)((k) => getDecryptedSetting(k));
93175
93247
  const emailSubject = getDecryptedSetting("outreach_email_subject") || "A free demo website for {name}";
93176
93248
  const prepared = [];
93249
+ let harvested = 0;
93177
93250
  for (const r of eligible) {
93178
93251
  const message = template.replace(/\{name\}/g, r.name).replace(/\{site_url\}/g, r.site_url ?? "");
93179
93252
  const e164 = toE164(r.phone ?? "");
93253
+ if (!e164 && !r.email && smtp) {
93254
+ ctx.emitProgress?.({
93255
+ channel: "outreach:progress",
93256
+ payload: { msg: `Looking up an email for ${r.name}\u2026` }
93257
+ });
93258
+ const found = await (0, email_harvest_1.harvestEmail)({
93259
+ name: r.name,
93260
+ address: r.address ?? "",
93261
+ city: r.city ?? "",
93262
+ apiKey: getDecryptedSetting("anthropic_key")
93263
+ });
93264
+ if (found) {
93265
+ db2.run("UPDATE leads SET email = ? WHERE id = ?", [found, r.id]);
93266
+ r.email = found;
93267
+ harvested++;
93268
+ }
93269
+ }
93180
93270
  if (e164) {
93181
93271
  prepared.push({
93182
93272
  id: r.id,
@@ -93227,6 +93317,7 @@ Interested?`;
93227
93317
  phoneLinks: prepared.filter((p) => p.via === "phone").length,
93228
93318
  emailsSent,
93229
93319
  emailsFailed,
93320
+ emailsFound: harvested,
93230
93321
  unreachable,
93231
93322
  // Guide the user if email-only leads were skipped for want of SMTP.
93232
93323
  note: smtpMissing ? "Some leads have only an email but SMTP isn't set up \u2014 run 'sproutkit setup' to add SMTP and email them." : void 0,
@@ -94328,7 +94419,7 @@ var require_package2 = __commonJS({
94328
94419
  "package.json"(exports2, module2) {
94329
94420
  module2.exports = {
94330
94421
  name: "sproutkit-code",
94331
- version: "0.15.0",
94422
+ version: "0.16.0",
94332
94423
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
94333
94424
  bin: {
94334
94425
  sproutkit: "./dist/index.js"
@@ -94589,10 +94680,10 @@ var Status = class {
94589
94680
  this.timer = null;
94590
94681
  }
94591
94682
  const label = finalLabel ?? this.label;
94592
- const mark = ok ? c.green("\u2713") : c.red("\u2717");
94593
- if (this.tty) process.stdout.write(`\r\x1B[2K ${mark} ${label}
94683
+ const mark2 = ok ? c.green("\u2713") : c.red("\u2717");
94684
+ if (this.tty) process.stdout.write(`\r\x1B[2K ${mark2} ${label}
94594
94685
  `);
94595
- else say(` ${mark} ${label}`);
94686
+ else say(` ${mark2} ${label}`);
94596
94687
  }
94597
94688
  };
94598
94689
  var STATE_MARK = {
@@ -94641,16 +94732,16 @@ var StatusBoard = class {
94641
94732
  else this.render();
94642
94733
  }
94643
94734
  line(r) {
94644
- const mark = r.state === "building" ? c.cyan(FRAMES[this.frame]) : STATE_MARK[r.state]();
94735
+ const mark2 = r.state === "building" ? c.cyan(FRAMES[this.frame]) : STATE_MARK[r.state]();
94645
94736
  const detail = r.detail ? c.dim(` \xB7 ${r.detail}`) : "";
94646
- return ` ${mark} ${r.label}${detail}`;
94737
+ return ` ${mark2} ${r.label}${detail}`;
94647
94738
  }
94648
94739
  logRowIfChanged(r) {
94649
94740
  const sig = `${r.state}:${r.detail ?? ""}`;
94650
94741
  if (this.lastSig.get(r.key) === sig) return;
94651
94742
  this.lastSig.set(r.key, sig);
94652
- const mark = r.state === "built" ? "\u2713" : r.state === "failed" ? "\u2717" : r.state === "skipped" ? "\u2013" : "\u2026";
94653
- say(` ${mark} ${r.label}${r.detail ? ` \xB7 ${r.detail}` : ""}`);
94743
+ const mark2 = r.state === "built" ? "\u2713" : r.state === "failed" ? "\u2717" : r.state === "skipped" ? "\u2013" : "\u2026";
94744
+ say(` ${mark2} ${r.label}${r.detail ? ` \xB7 ${r.detail}` : ""}`);
94654
94745
  }
94655
94746
  render() {
94656
94747
  if (this.lastLineCount > 0) process.stdout.write(`\x1B[${this.lastLineCount}A`);
@@ -95540,6 +95631,7 @@ async function cmdOutreach(argv) {
95540
95631
  say();
95541
95632
  const parts = [];
95542
95633
  if (result.phoneLinks) parts.push(`${result.phoneLinks} message link${result.phoneLinks === 1 ? "" : "s"} to open`);
95634
+ if (result.emailsFound) parts.push(c.dim(`${result.emailsFound} email${result.emailsFound === 1 ? "" : "s"} found`));
95543
95635
  if (result.emailsSent) parts.push(`${result.emailsSent} emailed`);
95544
95636
  if (result.emailsFailed) parts.push(c.red(`${result.emailsFailed} email failed`));
95545
95637
  if (result.unreachable) parts.push(c.amber(`${result.unreachable} unreachable`));
@@ -95845,13 +95937,136 @@ function runClaudeTurn(args) {
95845
95937
  });
95846
95938
  }
95847
95939
 
95848
- // src/index.ts
95940
+ // src/doctor.ts
95849
95941
  var import_child_process4 = require("child_process");
95942
+ function mark(l) {
95943
+ return l === "ok" ? c.green("\u2713") : l === "warn" ? c.amber("!") : c.red("\u2717");
95944
+ }
95945
+ async function runDoctor() {
95946
+ const rows = [];
95947
+ const backend = getBackend();
95948
+ const lic = await check();
95949
+ rows.push(
95950
+ lic.status === "active" ? { level: "ok", label: "Licence", detail: "active" } : {
95951
+ level: "fail",
95952
+ label: "Licence",
95953
+ detail: lic.status,
95954
+ fix: "Run 'sproutkit setup' and enter your key, or buy one at https://sproutkit.app"
95955
+ }
95956
+ );
95957
+ if (backend === "claude_code") {
95958
+ const present = (() => {
95959
+ try {
95960
+ return (0, import_child_process4.spawnSync)(claudeBin(), ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
95961
+ } catch {
95962
+ return false;
95963
+ }
95964
+ })();
95965
+ if (!present) {
95966
+ rows.push({
95967
+ level: "fail",
95968
+ label: "Claude Code",
95969
+ detail: "CLI not found",
95970
+ fix: "Install Claude Code, run 'claude /login', then 'sproutkit setup'."
95971
+ });
95972
+ } else {
95973
+ const auth = await checkClaudeCodeAuth();
95974
+ rows.push(
95975
+ auth.ok ? {
95976
+ level: "ok",
95977
+ label: "Claude (subscription)",
95978
+ detail: `logged in \xB7 model: ${getClaudeCodeModel() ?? "default"}`
95979
+ } : {
95980
+ level: "fail",
95981
+ label: "Claude (subscription)",
95982
+ detail: auth.reason,
95983
+ fix: "Run 'claude /login' in another terminal, then re-run doctor."
95984
+ }
95985
+ );
95986
+ }
95987
+ } else {
95988
+ rows.push(
95989
+ getSetting("anthropic_key") ? { level: "ok", label: "Anthropic API key", detail: "set" } : {
95990
+ level: "fail",
95991
+ label: "Anthropic API key",
95992
+ detail: "missing",
95993
+ fix: "Run 'sproutkit setup' (or switch to the Claude subscription backend)."
95994
+ }
95995
+ );
95996
+ }
95997
+ rows.push(
95998
+ getSetting("places_key") ? { level: "ok", label: "Google Places key", detail: "set \u2014 lead search works" } : {
95999
+ level: "fail",
96000
+ label: "Google Places key",
96001
+ detail: "missing",
96002
+ fix: "Run 'sproutkit setup'. Without it, 'find' can't search for leads."
96003
+ }
96004
+ );
96005
+ rows.push(
96006
+ getSetting("vercel_token") ? { level: "ok", label: "Vercel token", detail: "set \u2014 deploys work" } : {
96007
+ level: "fail",
96008
+ label: "Vercel token",
96009
+ detail: "missing",
96010
+ fix: "Run 'sproutkit setup'. Without it, sites can't deploy."
96011
+ }
96012
+ );
96013
+ rows.push(
96014
+ getSetting("blob_token") ? { level: "ok", label: "Vercel Blob token", detail: "set \u2014 booking & CMS work" } : {
96015
+ level: "warn",
96016
+ label: "Vercel Blob token",
96017
+ detail: "missing \u2014 booking sites & CMS will fail",
96018
+ fix: "Create a Blob store at https://vercel.com/dashboard/stores, copy BLOB_READ_WRITE_TOKEN, then 'sproutkit setup'. One token covers all your sites."
96019
+ }
96020
+ );
96021
+ const smtpReady = !!(getSetting("smtp_host") && getSetting("smtp_user") && getSetting("smtp_pass"));
96022
+ rows.push(
96023
+ smtpReady ? { level: "ok", label: "SMTP (email outreach)", detail: `via ${getSetting("smtp_host")}` } : {
96024
+ level: "warn",
96025
+ label: "SMTP (email outreach)",
96026
+ detail: "not set \u2014 leads without a phone can't be emailed",
96027
+ fix: "Run 'sproutkit setup' and add SMTP host/user/password."
96028
+ }
96029
+ );
96030
+ rows.push(
96031
+ getSetting("resend_key") ? { level: "ok", label: "Resend key", detail: "set \u2014 booking emails notify the owner" } : {
96032
+ level: "warn",
96033
+ label: "Resend key",
96034
+ detail: "not set \u2014 bookings are stored but the owner isn't emailed",
96035
+ fix: "Optional. Add it in 'sproutkit setup' if you want booking notifications."
96036
+ }
96037
+ );
96038
+ rows.push(
96039
+ skillInstalled() ? { level: "ok", label: "Claude Code skill", detail: skillTargetDir() } : {
96040
+ level: "warn",
96041
+ label: "Claude Code skill",
96042
+ detail: "not installed",
96043
+ fix: "Run 'sproutkit setup' and say yes to installing the skill, so `claude` can drive Sproutkit."
96044
+ }
96045
+ );
96046
+ say();
96047
+ say(c.greenBold(" Sproutkit doctor"));
96048
+ say(c.dim(` Backend: ${backend === "claude_code" ? "Claude subscription" : "Anthropic API key"}`));
96049
+ say();
96050
+ for (const r of rows) {
96051
+ say(` ${mark(r.level)} ${c.bold(r.label)}${r.detail ? c.dim(` \u2014 ${r.detail}`) : ""}`);
96052
+ if (r.fix && r.level !== "ok") say(c.dim(` \u2192 ${r.fix}`));
96053
+ }
96054
+ say();
96055
+ const fails = rows.filter((r) => r.level === "fail").length;
96056
+ const warns = rows.filter((r) => r.level === "warn").length;
96057
+ if (fails === 0 && warns === 0) say(c.green(" All good \u2014 you're ready to find leads and build sites."));
96058
+ else if (fails === 0) say(c.amber(` Ready to build, with ${warns} optional thing${warns === 1 ? "" : "s"} unset (see above).`));
96059
+ else say(c.red(` ${fails} blocking issue${fails === 1 ? "" : "s"} \u2014 fix those first.`));
96060
+ say();
96061
+ }
96062
+
96063
+ // src/index.ts
96064
+ var import_child_process5 = require("child_process");
95850
96065
  process.env.SPROUTKIT_HEADLESS = "1";
95851
96066
  function claudePresent() {
95852
96067
  try {
95853
96068
  const bin = process.env.SPROUTKIT_CLAUDE_BIN || "claude";
95854
- return (0, import_child_process4.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96069
+ return (0, import_child_process5.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
95855
96070
  } catch {
95856
96071
  return false;
95857
96072
  }
@@ -95892,6 +96107,11 @@ async function main() {
95892
96107
  closeTty();
95893
96108
  return;
95894
96109
  }
96110
+ if (arg === "doctor") {
96111
+ await runDoctor();
96112
+ closeTty();
96113
+ return;
96114
+ }
95895
96115
  if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach") {
95896
96116
  const subArgs = process.argv.slice(3);
95897
96117
  const lic2 = await check();
@@ -95929,6 +96149,7 @@ async function main() {
95929
96149
  say(` ${c.bold("sproutkit book <id>")} booking site (--standalone for own-URL)`);
95930
96150
  say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
95931
96151
  say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
96152
+ say(` ${c.bold("sproutkit doctor")} check everything is wired up`);
95932
96153
  say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
95933
96154
  say(` ${c.bold("sproutkit sync")} push your leads into the desktop app`);
95934
96155
  say(` ${c.bold("sproutkit update")} update to the latest version`);
@@ -95999,7 +96220,7 @@ async function main() {
95999
96220
  }
96000
96221
  const direct = line.replace(/^\//, "");
96001
96222
  const verb = direct.split(/\s+/)[0]?.toLowerCase();
96002
- const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach"];
96223
+ const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "doctor"];
96003
96224
  if (verb === "commands" || verb === "help" || line === "/help" || line === "/?") {
96004
96225
  say();
96005
96226
  say(c.bold(" Commands (type with or without a leading /):"));
@@ -96012,7 +96233,7 @@ async function main() {
96012
96233
  say(` ${c.greenBold("book")} <id> [--standalone] [--archetype slots|reservation|enquiry] [--email \u2026]`);
96013
96234
  say(` ${c.dim(" ")} booking on the site, or --standalone for its own URL`);
96014
96235
  say(` ${c.greenBold("outreach")} [--ids 3,5] prepare outreach links`);
96015
- say(c.dim(" /freemode \u2014 open conversation & custom tweaks \xB7 /taskmode \u2014 precise \xB7 exit \u2014 quit"));
96236
+ say(c.dim(" doctor \u2014 check your setup \xB7 /freemode \u2014 open chat \xB7 /taskmode \u2014 precise \xB7 exit"));
96016
96237
  say(c.dim(" Or just talk: \u201Cfind cafes in Leeds and build the best two\u201D."));
96017
96238
  say();
96018
96239
  continue;
@@ -96029,6 +96250,7 @@ async function main() {
96029
96250
  else if (verb === "cms") await cmdCms(argv);
96030
96251
  else if (verb === "book") await cmdBook(argv);
96031
96252
  else if (verb === "outreach") await cmdOutreach(argv);
96253
+ else if (verb === "doctor") await runDoctor();
96032
96254
  } catch (e) {
96033
96255
  say(c.red(` Error: ${e.message}`));
96034
96256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.15.0",
3
+ "version": "0.16.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"