sproutkit-code 1.11.0 → 1.12.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 +275 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -30655,7 +30655,7 @@ var require_llm = __commonJS({
30655
30655
  };
30656
30656
  if (args.allowWebSearch) {
30657
30657
  req.tools = [
30658
- { type: "web_search_20250305", name: "web_search", max_uses: 3 }
30658
+ { type: "web_search_20250305", name: "web_search", max_uses: args.maxSearches ?? 3 }
30659
30659
  ];
30660
30660
  }
30661
30661
  const res = await anthropic.messages.create(req);
@@ -81604,6 +81604,7 @@ var require_builder = __commonJS({
81604
81604
  var sdk_1 = __importDefault(require_sdk());
81605
81605
  var axios_1 = __importDefault(require_axios());
81606
81606
  var crypto_1 = require("crypto");
81607
+ var vm = __importStar(require("vm"));
81607
81608
  var runtime_1 = require_runtime();
81608
81609
  var db_1 = require_db();
81609
81610
  var llm_1 = require_llm();
@@ -81617,6 +81618,58 @@ var require_builder = __commonJS({
81617
81618
  var analytics_runtime_1 = require_analytics_runtime();
81618
81619
  var cms_runtime_1 = require_cms_runtime();
81619
81620
  var booking_runtime_1 = require_booking_runtime();
81621
+ function storedSetting(key) {
81622
+ try {
81623
+ const row = (0, db_1.getDb)().get("SELECT value FROM settings WHERE key = ?", [key]);
81624
+ return row ? (0, runtime_1.decryptSettingValue)(key, row.value) : null;
81625
+ } catch {
81626
+ return null;
81627
+ }
81628
+ }
81629
+ function extractFallback(code) {
81630
+ const m = /const\s+FALLBACK\s*=\s*\{/.exec(code);
81631
+ if (!m)
81632
+ return null;
81633
+ const start = m.index + m[0].length - 1;
81634
+ let depth = 0;
81635
+ let quote = null;
81636
+ for (let i = start; i < code.length; i++) {
81637
+ const ch = code[i];
81638
+ if (quote) {
81639
+ if (ch === "\\")
81640
+ i++;
81641
+ else if (ch === quote)
81642
+ quote = null;
81643
+ continue;
81644
+ }
81645
+ if (ch === "/" && code[i + 1] === "/") {
81646
+ i = code.indexOf("\n", i);
81647
+ if (i < 0)
81648
+ return null;
81649
+ continue;
81650
+ }
81651
+ if (ch === "/" && code[i + 1] === "*") {
81652
+ i = code.indexOf("*/", i) + 1;
81653
+ if (i <= 0)
81654
+ return null;
81655
+ continue;
81656
+ }
81657
+ if (ch === '"' || ch === "'" || ch === "`")
81658
+ quote = ch;
81659
+ else if (ch === "{")
81660
+ depth++;
81661
+ else if (ch === "}" && --depth === 0) {
81662
+ try {
81663
+ const v = vm.runInNewContext("(" + code.slice(start, i + 1) + ")", /* @__PURE__ */ Object.create(null), { timeout: 100 });
81664
+ const json = JSON.parse(JSON.stringify(v));
81665
+ return json && typeof json === "object" && json.business ? json : null;
81666
+ } catch {
81667
+ return null;
81668
+ }
81669
+ }
81670
+ }
81671
+ return null;
81672
+ }
81620
81673
  function siteUidFor(leadId) {
81621
81674
  const db2 = (0, db_1.getDb)();
81622
81675
  const row = db2.get("SELECT site_uid FROM leads WHERE id = ?", [leadId]);
@@ -81901,7 +81954,9 @@ var require_builder = __commonJS({
81901
81954
  }
81902
81955
  }
81903
81956
  async function buildAndDeploySite(args) {
81904
- const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, includeCms, blobToken, reuseAdminPassword, skipContentExtraction, multipage, pages, skipQa } = args;
81957
+ const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, reuseAdminPassword, skipContentExtraction, multipage, pages, skipQa } = args;
81958
+ const blobToken = args.blobToken ?? storedSetting("blob_token");
81959
+ const includeCms = args.includeCms ?? (!!blobToken && storedSetting("build_with_editor") !== "0");
81905
81960
  const pageList = multipage && (!pages || pages.length === 0) ? ["Home", "Services", "About", "Contact"] : pages ?? [];
81906
81961
  const credit = (0, agency_1.siteCredit)();
81907
81962
  const footerCredit = credit.url ? `Built by ${credit.label} \xB7 ${credit.url}` : `Built by ${credit.label}`;
@@ -81909,7 +81964,7 @@ var require_builder = __commonJS({
81909
81964
  throw new Error("CMS is enabled but no Vercel Blob token is set. Add it in Settings \u2192 Vercel Blob token.");
81910
81965
  }
81911
81966
  let details = { reviews: [], photoNames: [], hours: [], priceLevel: null };
81912
- if (lead.place_id && placesKey) {
81967
+ if (lead.place_id && !lead.place_id.startsWith("ai:") && placesKey) {
81913
81968
  onProgress("Fetching reviews and photos from Google...");
81914
81969
  details = await fetchPlaceDetails(lead.place_id, placesKey);
81915
81970
  }
@@ -82369,7 +82424,15 @@ export default defineConfig({ plugins: [react()] });`;
82369
82424
  theme: { variant: "premium-dark", accent: "#22c55e" },
82370
82425
  photos: photosArr
82371
82426
  };
82372
- const initialContent = cmsSeedContent ? {
82427
+ const fromCode = cmsSeedContent ? null : extractFallback(appCode);
82428
+ const initialContent = fromCode ? {
82429
+ ...defaultContent,
82430
+ ...fromCode,
82431
+ business: { ...defaultContent.business, ...fromCode.business },
82432
+ hero: { ...defaultContent.hero, ...fromCode.hero },
82433
+ contact: { ...defaultContent.contact, ...fromCode.contact },
82434
+ photos: photosArr
82435
+ } : cmsSeedContent ? {
82373
82436
  ...defaultContent,
82374
82437
  ...cmsSeedContent,
82375
82438
  business: {
@@ -84205,6 +84268,175 @@ var require_lead_search = __commonJS({
84205
84268
  }
84206
84269
  });
84207
84270
 
84271
+ // ../desktop/dist/main/lib/ai-search.js
84272
+ var require_ai_search = __commonJS({
84273
+ "../desktop/dist/main/lib/ai-search.js"(exports2) {
84274
+ "use strict";
84275
+ Object.defineProperty(exports2, "__esModule", { value: true });
84276
+ exports2.isAiPlaceId = isAiPlaceId;
84277
+ exports2.aiSearchLeads = aiSearchLeads;
84278
+ var crypto_1 = require("crypto");
84279
+ var db_1 = require_db();
84280
+ var llm_1 = require_llm();
84281
+ var models_1 = require_models();
84282
+ var places_1 = require_places();
84283
+ function isAiPlaceId(id) {
84284
+ return !!id && id.startsWith("ai:");
84285
+ }
84286
+ function aiPlaceId(name, city) {
84287
+ const key = `${name}|${city}`.toLowerCase().replace(/[^a-z0-9|]/g, "");
84288
+ return "ai:" + (0, crypto_1.createHash)("sha1").update(key).digest("hex").slice(0, 16);
84289
+ }
84290
+ function simplify(s) {
84291
+ return s.toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]/g, "");
84292
+ }
84293
+ function sameBusiness(a, b) {
84294
+ const x = simplify(a), y = simplify(b);
84295
+ return !!x && !!y && (x.includes(y) || y.includes(x));
84296
+ }
84297
+ function parseFound(text) {
84298
+ const body = text.match(/<leads>\s*([\s\S]*?)\s*<\/leads>/)?.[1] ?? text.match(/\[[\s\S]*\]/)?.[0];
84299
+ if (!body)
84300
+ return [];
84301
+ try {
84302
+ const raw = JSON.parse(body.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, ""));
84303
+ if (!Array.isArray(raw))
84304
+ return [];
84305
+ return raw.map((r) => ({
84306
+ name: String(r?.name ?? "").trim(),
84307
+ type: String(r?.type ?? "").trim(),
84308
+ address: String(r?.address ?? "").trim(),
84309
+ phone: String(r?.phone ?? "").trim(),
84310
+ source_url: String(r?.source_url ?? "").trim()
84311
+ })).filter((r) => r.name.length > 1);
84312
+ } catch {
84313
+ return [];
84314
+ }
84315
+ }
84316
+ async function aiSearchLeads(args, onLead) {
84317
+ const db2 = (0, db_1.getDb)();
84318
+ const cap = Math.max(1, Math.min(args.maxResults ?? 20, 40));
84319
+ const progress = args.onProgress ?? (() => {
84320
+ });
84321
+ const kinds = args.types.length ? args.types.join(", ") : "local service businesses";
84322
+ progress(`Searching the web for ${kinds} in ${args.city}\u2026`);
84323
+ const prompt = `Find real, currently trading local businesses in ${args.city} that do NOT have a website of their own. Types: ${kinds}.
84324
+
84325
+ Search the web: business directories (Yell, Thomson Local, Checkatrade, Bark, FreeIndex, Scoot), Facebook and Instagram business pages, Google Maps listings, local community pages. A business that only has a Facebook page, Instagram, or a directory listing counts. A business with its own domain (e.g. kingscuts.co.uk), a Wix/Squarespace site, or a page on a franchise or chain's site does NOT count \u2014 leave it out. Leave out national chains.
84326
+
84327
+ Find up to ${cap}. Only include a business you actually saw on a page during this search, and give that page's URL. Copy the phone number and address exactly as shown; use "" if the page doesn't show one. Never guess or invent details.
84328
+
84329
+ Reply with only this, no other text:
84330
+ <leads>
84331
+ [{ "name": "", "type": "one of: ${kinds}", "address": "", "phone": "", "source_url": "" }]
84332
+ </leads>`;
84333
+ const res = await (0, llm_1.generateText)({
84334
+ apiKey: args.anthropicKey,
84335
+ prompt,
84336
+ model: (0, models_1.getPreferredModel)(),
84337
+ maxTokens: 8e3,
84338
+ allowWebSearch: true,
84339
+ maxSearches: 10,
84340
+ timeoutMs: 42e4
84341
+ });
84342
+ const found = parseFound(res.text ?? "");
84343
+ if (found.length === 0) {
84344
+ return {
84345
+ found: 0,
84346
+ saved: 0,
84347
+ errors: [res.text ? "AI search didn't return any businesses. Try a bigger town or different business types." : "AI search returned nothing."]
84348
+ };
84349
+ }
84350
+ const existing = db2.all("SELECT name, city, place_id FROM leads", []);
84351
+ const known = new Set(existing.map((e) => e.place_id).filter(Boolean));
84352
+ const inPipeline = (name) => existing.some((e) => sameBusiness(e.name, name) && (!e.city || sameBusiness(e.city, args.city)));
84353
+ let saved = 0, withWebsite = 0, alreadyInPipeline = 0;
84354
+ for (const f of found) {
84355
+ if (saved >= cap)
84356
+ break;
84357
+ if (inPipeline(f.name)) {
84358
+ alreadyInPipeline++;
84359
+ continue;
84360
+ }
84361
+ let lead = {
84362
+ place_id: aiPlaceId(f.name, args.city),
84363
+ name: f.name,
84364
+ type: f.type || args.types[0] || "local business",
84365
+ address: f.address,
84366
+ phone: f.phone,
84367
+ rating: null,
84368
+ review_count: null,
84369
+ lat: null,
84370
+ lng: null,
84371
+ price_level: null,
84372
+ business_status: null,
84373
+ has_hours: false
84374
+ };
84375
+ if (args.placesKey) {
84376
+ progress(`Checking ${f.name} on Google\u2026`);
84377
+ const g = await (0, places_1.findOneBusiness)({
84378
+ apiKey: args.placesKey,
84379
+ query: `${f.name} ${f.address || args.city}`
84380
+ });
84381
+ if (g && sameBusiness(g.name, f.name)) {
84382
+ if (g.website) {
84383
+ withWebsite++;
84384
+ continue;
84385
+ }
84386
+ lead = { ...lead, ...g, type: lead.type, phone: g.phone || f.phone, address: g.address || f.address };
84387
+ }
84388
+ }
84389
+ if (known.has(lead.place_id)) {
84390
+ alreadyInPipeline++;
84391
+ continue;
84392
+ }
84393
+ if (lead.lat == null && lead.address) {
84394
+ try {
84395
+ const [hit] = await (0, places_1.geocodeForward)(lead.address, 1);
84396
+ if (hit) {
84397
+ lead.lat = hit.lat;
84398
+ lead.lng = hit.lng;
84399
+ }
84400
+ } catch {
84401
+ }
84402
+ await new Promise((r) => setTimeout(r, 1100));
84403
+ }
84404
+ known.add(lead.place_id);
84405
+ const score = (0, places_1.scoreLead)({
84406
+ rating: lead.rating,
84407
+ review_count: lead.review_count,
84408
+ price_level: lead.price_level,
84409
+ phone: lead.phone,
84410
+ has_hours: lead.has_hours,
84411
+ business_status: lead.business_status
84412
+ });
84413
+ db2.run(`INSERT INTO leads
84414
+ (place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status, lead_kind, notes)
84415
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, 'discovered', 'no_website', ?)`, [
84416
+ lead.place_id,
84417
+ lead.name,
84418
+ lead.type,
84419
+ args.city,
84420
+ lead.address,
84421
+ lead.phone,
84422
+ lead.rating,
84423
+ lead.review_count,
84424
+ lead.lat,
84425
+ lead.lng,
84426
+ score,
84427
+ lead.price_level,
84428
+ lead.business_status,
84429
+ f.source_url ? `Found by AI search: ${f.source_url}` : "Found by AI search"
84430
+ ]);
84431
+ const row = db2.get("SELECT id FROM leads WHERE place_id = ?", [lead.place_id]);
84432
+ saved++;
84433
+ onLead?.({ id: row?.id ?? 0, place_id: lead.place_id, name: lead.name, type: lead.type, city: args.city, website: null });
84434
+ }
84435
+ return { found: found.length, saved, withWebsite, alreadyInPipeline };
84436
+ }
84437
+ }
84438
+ });
84439
+
84208
84440
  // ../desktop/dist/main/lib/followup.js
84209
84441
  var require_followup = __commonJS({
84210
84442
  "../desktop/dist/main/lib/followup.js"(exports2) {
@@ -97027,6 +97259,7 @@ var require_chat_tools = __commonJS({
97027
97259
  var runtime_1 = require_runtime();
97028
97260
  var builder_1 = require_builder();
97029
97261
  var lead_search_1 = require_lead_search();
97262
+ var ai_search_1 = require_ai_search();
97030
97263
  var places_1 = require_places();
97031
97264
  var booking_detect_1 = require_booking_detect();
97032
97265
  var site_audit_1 = require_site_audit();
@@ -97072,6 +97305,24 @@ var require_chat_tools = __commonJS({
97072
97305
  },
97073
97306
  requiresConfirmation: true
97074
97307
  },
97308
+ {
97309
+ name: "ai_search_leads",
97310
+ description: "AI search: Claude searches the web (directories, Facebook/Instagram pages, Yell, Checkatrade) for local businesses with NO website, instead of Google Places. Use when the user asks for AI search, has no Google Places key, or Places found too few. Slower (1\u20133 minutes), runs on the user's Claude plan, max 40 per run. With a Places key set, each result is checked on Google and dropped if it has a website. Saves new leads like find_leads.",
97311
+ input_schema: {
97312
+ type: "object",
97313
+ properties: {
97314
+ location: { type: "string", description: "Town, city, postcode or area, e.g. 'Cardiff'." },
97315
+ categories: {
97316
+ type: "array",
97317
+ items: { type: "string" },
97318
+ description: "Business types, e.g. ['plumber', 'barber shop']."
97319
+ },
97320
+ maxResults: { type: "number", description: "Maximum leads to save (default 20, max 40)." }
97321
+ },
97322
+ required: ["location", "categories"]
97323
+ },
97324
+ requiresConfirmation: true
97325
+ },
97075
97326
  {
97076
97327
  name: "list_leads",
97077
97328
  description: "List all leads, optionally filtered by status (discovered, building, site_built, contacted, replied, won, dead) or city. Returns id, name, status, type, city, phone, site_url.",
@@ -97598,6 +97849,22 @@ If you'd rather not hear from me again, just reply and let me know.`;
97598
97849
  note: result.saved === 0 && result.found > 0 ? "All matches were already in your pipeline \u2014 nothing new saved." : void 0
97599
97850
  };
97600
97851
  }
97852
+ case "ai_search_leads": {
97853
+ const location = String(input.location ?? "").trim();
97854
+ const categories = input.categories ?? [];
97855
+ if (!location)
97856
+ return { ok: false, error: "A location is required." };
97857
+ const result = await (0, ai_search_1.aiSearchLeads)({
97858
+ anthropicKey: getDecryptedSetting("anthropic_key"),
97859
+ placesKey: getDecryptedSetting("places_key"),
97860
+ city: location,
97861
+ types: categories,
97862
+ maxResults: Math.min(Number(input.maxResults ?? 20), 40),
97863
+ onProgress: (msg) => ctx.emitProgress?.({ channel: "leads:searching", payload: { msg } })
97864
+ }, (lead) => ctx.emitProgress?.({ channel: "leads:found", payload: { lead } }));
97865
+ ctx.emitProgress?.({ channel: "leads:changed", payload: { reason: "ai_search_leads", ...result } });
97866
+ return { ...result, location, categories, source: "ai" };
97867
+ }
97601
97868
  case "list_leads": {
97602
97869
  const status = input.status;
97603
97870
  const city = input.city;
@@ -98245,7 +98512,7 @@ Tools available:
98245
98512
  - list_leads, get_lead, summarise_pipeline (read-only)
98246
98513
  - move_lead, whatsapp_message (low-risk writes)
98247
98514
  - prepare_outreach (fills the template, moves leads to Contacted, returns links to open)
98248
- - find_leads, build_site, build_all_sites, regenerate_site, add_booking_site, provision_cms, delete_lead (spend money or change things \u2014 require confirmation)
98515
+ - find_leads, ai_search_leads, build_site, build_all_sites, regenerate_site, add_booking_site, provision_cms, delete_lead (spend money or change things \u2014 require confirmation)
98249
98516
 
98250
98517
  When the user asks you to do something, prefer USING the tools to describing how. Examples:
98251
98518
  - "How many leads do I have in Won?" \u2192 call summarise_pipeline
@@ -98277,7 +98544,7 @@ Choosing between build_site and build_all_sites:
98277
98544
  The user may send messages that begin with a slash command, e.g. "/find-leads barber shops in Shoreditch", "/build Ada's Barber Shop", "/build *", "/pipeline". Treat these as ordinary instructions and map them to the matching tool. "/build *" means build everything unbuilt \u2192 build_all_sites. If a slash command arrives with no arguments, ask for the missing detail rather than guessing.
98278
98545
 
98279
98546
  Confirmation rules:
98280
- - For tools that spend money or change things (find_leads, build_site, build_all_sites, regenerate_site, add_booking_site, provision_cms, delete_lead) the system pauses and asks the user to click Confirm. Don't ask "shall I?" \u2014 just call the tool, the UI handles confirmation.
98547
+ - For tools that spend money or change things (find_leads, ai_search_leads, build_site, build_all_sites, regenerate_site, add_booking_site, provision_cms, delete_lead) the system pauses and asks the user to click Confirm. Don't ask "shall I?" \u2014 just call the tool, the UI handles confirmation.
98281
98548
  - For non-destructive tools, run them immediately.
98282
98549
  - Do mention the rough cost when it's relevant: lead searches cost about \xA30.025 per category, a site build about \xA30.02 in Anthropic credits.
98283
98550
 
@@ -98617,7 +98884,7 @@ var require_auto_mode = __commonJS({
98617
98884
  }
98618
98885
  }
98619
98886
  });
98620
- db2.run("UPDATE leads SET site_url = ?, status = 'site_built', site_built_at = datetime('now') WHERE id = ?", [result.url, leadId]);
98887
+ db2.run("UPDATE leads SET site_url = ?, status = 'site_built', site_built_at = datetime('now'), admin_url = ?, admin_password = ? WHERE id = ?", [result.url, result.admin?.url ?? null, result.admin?.password ?? null, leadId]);
98621
98888
  state.status = "built";
98622
98889
  state.siteUrl = result.url;
98623
98890
  state.message = "Built";
@@ -99728,7 +99995,7 @@ var require_package2 = __commonJS({
99728
99995
  "package.json"(exports2, module2) {
99729
99996
  module2.exports = {
99730
99997
  name: "sproutkit-code",
99731
- version: "1.11.0",
99998
+ version: "1.12.0",
99732
99999
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
99733
100000
  bin: {
99734
100001
  sproutkit: "./dist/index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "1.11.0",
3
+ "version": "1.12.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"