sproutkit-code 1.11.1 → 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.
- package/dist/index.js +209 -5
- 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);
|
|
@@ -81964,7 +81964,7 @@ var require_builder = __commonJS({
|
|
|
81964
81964
|
throw new Error("CMS is enabled but no Vercel Blob token is set. Add it in Settings \u2192 Vercel Blob token.");
|
|
81965
81965
|
}
|
|
81966
81966
|
let details = { reviews: [], photoNames: [], hours: [], priceLevel: null };
|
|
81967
|
-
if (lead.place_id && placesKey) {
|
|
81967
|
+
if (lead.place_id && !lead.place_id.startsWith("ai:") && placesKey) {
|
|
81968
81968
|
onProgress("Fetching reviews and photos from Google...");
|
|
81969
81969
|
details = await fetchPlaceDetails(lead.place_id, placesKey);
|
|
81970
81970
|
}
|
|
@@ -84268,6 +84268,175 @@ var require_lead_search = __commonJS({
|
|
|
84268
84268
|
}
|
|
84269
84269
|
});
|
|
84270
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
|
+
|
|
84271
84440
|
// ../desktop/dist/main/lib/followup.js
|
|
84272
84441
|
var require_followup = __commonJS({
|
|
84273
84442
|
"../desktop/dist/main/lib/followup.js"(exports2) {
|
|
@@ -97090,6 +97259,7 @@ var require_chat_tools = __commonJS({
|
|
|
97090
97259
|
var runtime_1 = require_runtime();
|
|
97091
97260
|
var builder_1 = require_builder();
|
|
97092
97261
|
var lead_search_1 = require_lead_search();
|
|
97262
|
+
var ai_search_1 = require_ai_search();
|
|
97093
97263
|
var places_1 = require_places();
|
|
97094
97264
|
var booking_detect_1 = require_booking_detect();
|
|
97095
97265
|
var site_audit_1 = require_site_audit();
|
|
@@ -97135,6 +97305,24 @@ var require_chat_tools = __commonJS({
|
|
|
97135
97305
|
},
|
|
97136
97306
|
requiresConfirmation: true
|
|
97137
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
|
+
},
|
|
97138
97326
|
{
|
|
97139
97327
|
name: "list_leads",
|
|
97140
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.",
|
|
@@ -97661,6 +97849,22 @@ If you'd rather not hear from me again, just reply and let me know.`;
|
|
|
97661
97849
|
note: result.saved === 0 && result.found > 0 ? "All matches were already in your pipeline \u2014 nothing new saved." : void 0
|
|
97662
97850
|
};
|
|
97663
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
|
+
}
|
|
97664
97868
|
case "list_leads": {
|
|
97665
97869
|
const status = input.status;
|
|
97666
97870
|
const city = input.city;
|
|
@@ -98308,7 +98512,7 @@ Tools available:
|
|
|
98308
98512
|
- list_leads, get_lead, summarise_pipeline (read-only)
|
|
98309
98513
|
- move_lead, whatsapp_message (low-risk writes)
|
|
98310
98514
|
- prepare_outreach (fills the template, moves leads to Contacted, returns links to open)
|
|
98311
|
-
- 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)
|
|
98312
98516
|
|
|
98313
98517
|
When the user asks you to do something, prefer USING the tools to describing how. Examples:
|
|
98314
98518
|
- "How many leads do I have in Won?" \u2192 call summarise_pipeline
|
|
@@ -98340,7 +98544,7 @@ Choosing between build_site and build_all_sites:
|
|
|
98340
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.
|
|
98341
98545
|
|
|
98342
98546
|
Confirmation rules:
|
|
98343
|
-
- 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.
|
|
98344
98548
|
- For non-destructive tools, run them immediately.
|
|
98345
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.
|
|
98346
98550
|
|
|
@@ -99791,7 +99995,7 @@ var require_package2 = __commonJS({
|
|
|
99791
99995
|
"package.json"(exports2, module2) {
|
|
99792
99996
|
module2.exports = {
|
|
99793
99997
|
name: "sproutkit-code",
|
|
99794
|
-
version: "1.
|
|
99998
|
+
version: "1.12.0",
|
|
99795
99999
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
99796
100000
|
bin: {
|
|
99797
100001
|
sproutkit: "./dist/index.js"
|