sproutkit-code 1.0.0 → 1.2.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 +431 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -195,6 +195,12 @@ var require_db = __commonJS({
|
|
|
195
195
|
db2.exec("ALTER TABLE leads ADD COLUMN email TEXT");
|
|
196
196
|
} catch {
|
|
197
197
|
}
|
|
198
|
+
for (const col of ["lead_kind TEXT", "booking_status TEXT", "booking_platform TEXT"]) {
|
|
199
|
+
try {
|
|
200
|
+
db2.exec(`ALTER TABLE leads ADD COLUMN ${col}`);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
198
204
|
}
|
|
199
205
|
function getDb2() {
|
|
200
206
|
return db2;
|
|
@@ -79738,6 +79744,13 @@ var require_builder = __commonJS({
|
|
|
79738
79744
|
const pattern = new RegExp(`https?:\\/\\/[^\\s"'\`)]*(${STOCK_HOSTS.map((h) => h.replace(/\./g, "\\.")).join("|")})[^\\s"'\`)]*`, "gi");
|
|
79739
79745
|
return code.replace(pattern, "");
|
|
79740
79746
|
}
|
|
79747
|
+
function demoBanner(businessName) {
|
|
79748
|
+
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
79749
|
+
const label = `Preview site for ${esc(businessName)}`;
|
|
79750
|
+
return `<div id="sproutkit-demo-banner" role="note" style="all:initial;display:block;box-sizing:border-box;width:100%;background:#1f2937;color:#f9fafb;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;font-size:13px;line-height:1.4;padding:10px 16px;text-align:center;letter-spacing:.01em">
|
|
79751
|
+
<strong style="all:unset;font-weight:600">DEMO</strong> \xB7 ${label} \u2014 not licensed for live use. Built with <a href="https://sproutkit.app" style="all:unset;color:#a7f3d0;text-decoration:underline;cursor:pointer">Sproutkit</a>. Contact us to go live with your own domain.
|
|
79752
|
+
</div>`;
|
|
79753
|
+
}
|
|
79741
79754
|
function snapshotDir(leadId) {
|
|
79742
79755
|
return path3.join((0, runtime_1.userDataDir)(), "data", "sites", String(leadId));
|
|
79743
79756
|
}
|
|
@@ -80311,7 +80324,7 @@ ${prompt}` : prompt;
|
|
|
80311
80324
|
onProgress("Deploying to Vercel...");
|
|
80312
80325
|
const indexHtml = `<!DOCTYPE html>
|
|
80313
80326
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head>
|
|
80314
|
-
<body style="margin:0"
|
|
80327
|
+
<body style="margin:0">${demoBanner(lead.name)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
80315
80328
|
const mainJsx = `import { StrictMode } from 'react';
|
|
80316
80329
|
import { createRoot } from 'react-dom/client';
|
|
80317
80330
|
import App from './App.jsx';
|
|
@@ -80506,7 +80519,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80506
80519
|
{ source: "/book/", destination: "/book/index.html" }
|
|
80507
80520
|
]
|
|
80508
80521
|
});
|
|
80509
|
-
const indexHtmlCms = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0"
|
|
80522
|
+
const indexHtmlCms = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${demoBanner(lead.name)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
80510
80523
|
const mainJsxCms = `import { StrictMode } from 'react';
|
|
80511
80524
|
import { createRoot } from 'react-dom/client';
|
|
80512
80525
|
import App from './App.jsx';
|
|
@@ -80583,7 +80596,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80583
80596
|
snapshot[appJsxIndex] = { ...snapshot[appJsxIndex], data: modifiedApp };
|
|
80584
80597
|
saveSnapshot(lead.id, snapshot);
|
|
80585
80598
|
const files = snapshot.map((f) => ({ file: f.file, data: f.data, encoding: f.encoding }));
|
|
80586
|
-
const indexHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0"
|
|
80599
|
+
const indexHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head><body style="margin:0">${demoBanner(lead.name)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
|
|
80587
80600
|
const mainJsx = `import { StrictMode } from 'react';
|
|
80588
80601
|
import { createRoot } from 'react-dom/client';
|
|
80589
80602
|
import App from './App.jsx';
|
|
@@ -80954,6 +80967,7 @@ var require_places = __commonJS({
|
|
|
80954
80967
|
exports2.scoreLead = scoreLead;
|
|
80955
80968
|
exports2.searchPlacesStream = searchPlacesStream;
|
|
80956
80969
|
exports2.searchPlaces = searchPlaces;
|
|
80970
|
+
exports2.findOneBusiness = findOneBusiness;
|
|
80957
80971
|
exports2.geocodeForward = geocodeForward;
|
|
80958
80972
|
exports2.geocodeReverse = geocodeReverse;
|
|
80959
80973
|
var axios_1 = __importDefault(require_axios());
|
|
@@ -81020,10 +81034,10 @@ var require_places = __commonJS({
|
|
|
81020
81034
|
}
|
|
81021
81035
|
return { type: args.type, places: all };
|
|
81022
81036
|
}
|
|
81023
|
-
function processRawPlaces(type, places, center, radiusMeters) {
|
|
81037
|
+
function processRawPlaces(type, places, center, radiusMeters, includeWithWebsite) {
|
|
81024
81038
|
const out = [];
|
|
81025
81039
|
for (const p of places) {
|
|
81026
|
-
if (p.websiteUri)
|
|
81040
|
+
if (p.websiteUri && !includeWithWebsite)
|
|
81027
81041
|
continue;
|
|
81028
81042
|
if (center && radiusMeters && p.location) {
|
|
81029
81043
|
const d = haversineMeters(center.lat, center.lng, p.location.latitude, p.location.longitude);
|
|
@@ -81064,7 +81078,7 @@ var require_places = __commonJS({
|
|
|
81064
81078
|
center: args.center,
|
|
81065
81079
|
radiusMeters: args.radiusMeters
|
|
81066
81080
|
});
|
|
81067
|
-
await onBatch(processRawPlaces(type, places, args.center, args.radiusMeters));
|
|
81081
|
+
await onBatch(processRawPlaces(type, places, args.center, args.radiusMeters, args.includeWithWebsite));
|
|
81068
81082
|
}));
|
|
81069
81083
|
}
|
|
81070
81084
|
async function searchPlaces(args) {
|
|
@@ -81132,6 +81146,23 @@ var require_places = __commonJS({
|
|
|
81132
81146
|
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
|
81133
81147
|
return 2 * R * Math.asin(Math.sqrt(a));
|
|
81134
81148
|
}
|
|
81149
|
+
async function findOneBusiness(args) {
|
|
81150
|
+
try {
|
|
81151
|
+
const r = await axios_1.default.post("https://places.googleapis.com/v1/places:searchText", { textQuery: args.query, maxResultCount: 1 }, {
|
|
81152
|
+
headers: {
|
|
81153
|
+
"X-Goog-Api-Key": args.apiKey,
|
|
81154
|
+
"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,places.primaryTypeDisplayName"
|
|
81155
|
+
}
|
|
81156
|
+
});
|
|
81157
|
+
const p = (r.data.places ?? [])[0];
|
|
81158
|
+
if (!p)
|
|
81159
|
+
return null;
|
|
81160
|
+
const results = processRawPlaces(p.primaryTypeDisplayName?.text || "local business", [p], void 0, void 0, true);
|
|
81161
|
+
return results[0] ?? null;
|
|
81162
|
+
} catch {
|
|
81163
|
+
return null;
|
|
81164
|
+
}
|
|
81165
|
+
}
|
|
81135
81166
|
async function geocodeForward(query, limit = 5) {
|
|
81136
81167
|
if (!query.trim())
|
|
81137
81168
|
return [];
|
|
@@ -81156,6 +81187,99 @@ var require_places = __commonJS({
|
|
|
81156
81187
|
}
|
|
81157
81188
|
});
|
|
81158
81189
|
|
|
81190
|
+
// ../desktop/dist/main/lib/booking-detect.js
|
|
81191
|
+
var require_booking_detect = __commonJS({
|
|
81192
|
+
"../desktop/dist/main/lib/booking-detect.js"(exports2) {
|
|
81193
|
+
"use strict";
|
|
81194
|
+
var __importDefault = exports2 && exports2.__importDefault || function(mod) {
|
|
81195
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
81196
|
+
};
|
|
81197
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
81198
|
+
exports2.checkBookingOnSite = checkBookingOnSite;
|
|
81199
|
+
exports2.isBookingProspect = isBookingProspect;
|
|
81200
|
+
var axios_1 = __importDefault(require_axios());
|
|
81201
|
+
var BOOKING_PLATFORMS = [
|
|
81202
|
+
{ re: /fresha\.com/i, name: "Fresha" },
|
|
81203
|
+
{ re: /treatwell|wahanda/i, name: "Treatwell" },
|
|
81204
|
+
{ re: /booksy\./i, name: "Booksy" },
|
|
81205
|
+
{ re: /opentable\./i, name: "OpenTable" },
|
|
81206
|
+
{ re: /calendly\.com/i, name: "Calendly" },
|
|
81207
|
+
{ re: /simplybook\.(me|it)/i, name: "SimplyBook" },
|
|
81208
|
+
{ re: /setmore\.com/i, name: "Setmore" },
|
|
81209
|
+
{ re: /squareup\.com\/(appointments|book)/i, name: "Square Appointments" },
|
|
81210
|
+
{ re: /acuityscheduling\.com|squarespacescheduling/i, name: "Acuity" },
|
|
81211
|
+
{ re: /resdiary\.com/i, name: "ResDiary" },
|
|
81212
|
+
{ re: /quandoo\./i, name: "Quandoo" },
|
|
81213
|
+
{ re: /thefork\.|lafourchette/i, name: "TheFork" },
|
|
81214
|
+
{ re: /dojo\.tech|dojobookings/i, name: "Dojo" },
|
|
81215
|
+
{ re: /bookwhen\.com/i, name: "Bookwhen" },
|
|
81216
|
+
{ re: /timely\.?app|gettimely\.com/i, name: "Timely" },
|
|
81217
|
+
{ re: /phorest/i, name: "Phorest" },
|
|
81218
|
+
{ re: /shedul\.com/i, name: "Shedul" },
|
|
81219
|
+
{ re: /mindbodyonline\.com/i, name: "Mindbody" },
|
|
81220
|
+
{ re: /ovatu\.com/i, name: "Ovatu" },
|
|
81221
|
+
{ re: /schedulicity\.com/i, name: "Schedulicity" },
|
|
81222
|
+
{ re: /appointedd\.com/i, name: "Appointedd" },
|
|
81223
|
+
{ re: /tablein\.com/i, name: "Tablein" },
|
|
81224
|
+
{ re: /eveve\.com/i, name: "Eveve" },
|
|
81225
|
+
{ re: /sevenrooms\.com/i, name: "SevenRooms" },
|
|
81226
|
+
{ re: /booking\.com\/hotel/i, name: "Booking.com" },
|
|
81227
|
+
{ re: /wix\.com\/bookings|wixapps.*booking/i, name: "Wix Bookings" },
|
|
81228
|
+
{ re: /woocommerce-bookings|wc-bookings/i, name: "WooCommerce Bookings" },
|
|
81229
|
+
{ re: /amelia(-booking)?|bookly/i, name: "WordPress booking plugin" }
|
|
81230
|
+
];
|
|
81231
|
+
var MESSAGING_ONLY = [
|
|
81232
|
+
{ re: /wa\.me\/|api\.whatsapp\.com|whatsapp:\/\//i, name: "WhatsApp" },
|
|
81233
|
+
{ re: /instagram\.com\/(?!p\/)/i, name: "Instagram DM" },
|
|
81234
|
+
{ re: /m\.me\/|messenger\.com/i, name: "Messenger" }
|
|
81235
|
+
];
|
|
81236
|
+
async function checkBookingOnSite(websiteUrl) {
|
|
81237
|
+
if (!websiteUrl || !/^https?:\/\//i.test(websiteUrl))
|
|
81238
|
+
return { verdict: "unknown" };
|
|
81239
|
+
let html = "";
|
|
81240
|
+
try {
|
|
81241
|
+
const res = await axios_1.default.get(websiteUrl, {
|
|
81242
|
+
timeout: 15e3,
|
|
81243
|
+
maxRedirects: 5,
|
|
81244
|
+
responseType: "text",
|
|
81245
|
+
// Some hosts serve a bot-block page to a bare client.
|
|
81246
|
+
headers: {
|
|
81247
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
|
|
81248
|
+
Accept: "text/html,application/xhtml+xml"
|
|
81249
|
+
},
|
|
81250
|
+
// Don't choke on huge pages; the signals appear early enough.
|
|
81251
|
+
maxContentLength: 3e6,
|
|
81252
|
+
validateStatus: (s) => s >= 200 && s < 400
|
|
81253
|
+
});
|
|
81254
|
+
html = typeof res.data === "string" ? res.data : String(res.data ?? "");
|
|
81255
|
+
} catch {
|
|
81256
|
+
return { verdict: "unknown" };
|
|
81257
|
+
}
|
|
81258
|
+
if (!html)
|
|
81259
|
+
return { verdict: "unknown" };
|
|
81260
|
+
const platform = BOOKING_PLATFORMS.find((p) => p.re.test(html));
|
|
81261
|
+
if (platform)
|
|
81262
|
+
return { verdict: "has_online_booking", detail: platform.name };
|
|
81263
|
+
const wantsBookings = /book\s*(a|an|now|online|table|appointment)|reservation|appointment|make a booking/i.test(html);
|
|
81264
|
+
const messaging = MESSAGING_ONLY.find((m) => {
|
|
81265
|
+
const hit = m.re.exec(html);
|
|
81266
|
+
if (!hit)
|
|
81267
|
+
return false;
|
|
81268
|
+
const from = Math.max(0, hit.index - 300);
|
|
81269
|
+
const window2 = html.slice(from, hit.index + 300);
|
|
81270
|
+
return /book|reserv|appointment|enquir|order/i.test(window2);
|
|
81271
|
+
});
|
|
81272
|
+
if (messaging) {
|
|
81273
|
+
return { verdict: "messaging_only", detail: messaging.name, wantsBookings };
|
|
81274
|
+
}
|
|
81275
|
+
return { verdict: "no_booking", wantsBookings };
|
|
81276
|
+
}
|
|
81277
|
+
function isBookingProspect(check2) {
|
|
81278
|
+
return check2.verdict === "messaging_only" || check2.verdict === "no_booking";
|
|
81279
|
+
}
|
|
81280
|
+
}
|
|
81281
|
+
});
|
|
81282
|
+
|
|
81159
81283
|
// ../desktop/dist/main/lib/lead-search.js
|
|
81160
81284
|
var require_lead_search = __commonJS({
|
|
81161
81285
|
"../desktop/dist/main/lib/lead-search.js"(exports2) {
|
|
@@ -81165,10 +81289,12 @@ var require_lead_search = __commonJS({
|
|
|
81165
81289
|
exports2.searchAndSaveLeads = searchAndSaveLeads;
|
|
81166
81290
|
var db_1 = require_db();
|
|
81167
81291
|
var places_1 = require_places();
|
|
81292
|
+
var booking_detect_1 = require_booking_detect();
|
|
81168
81293
|
async function searchAndSaveLeadsStream(args, onLead) {
|
|
81169
81294
|
const db2 = (0, db_1.getDb)();
|
|
81170
81295
|
const cap = args.maxResults ?? 50;
|
|
81171
81296
|
const seen = /* @__PURE__ */ new Set();
|
|
81297
|
+
const bookingMode = args.mode === "no_booking";
|
|
81172
81298
|
let saved = 0;
|
|
81173
81299
|
let found = 0;
|
|
81174
81300
|
await (0, places_1.searchPlacesStream)({
|
|
@@ -81176,8 +81302,10 @@ var require_lead_search = __commonJS({
|
|
|
81176
81302
|
city: args.city,
|
|
81177
81303
|
types: args.types,
|
|
81178
81304
|
center: args.center,
|
|
81179
|
-
radiusMeters: args.radiusMeters
|
|
81180
|
-
|
|
81305
|
+
radiusMeters: args.radiusMeters,
|
|
81306
|
+
// Booking mode needs the businesses that DO have a website.
|
|
81307
|
+
includeWithWebsite: bookingMode
|
|
81308
|
+
}, async (batch) => {
|
|
81181
81309
|
for (const r of batch) {
|
|
81182
81310
|
found++;
|
|
81183
81311
|
if (saved >= cap)
|
|
@@ -81188,9 +81316,25 @@ var require_lead_search = __commonJS({
|
|
|
81188
81316
|
const existing = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
81189
81317
|
if (existing.length > 0)
|
|
81190
81318
|
continue;
|
|
81319
|
+
let leadKind = "no_website";
|
|
81320
|
+
let bookingStatus = null;
|
|
81321
|
+
let bookingPlatform = null;
|
|
81322
|
+
if (bookingMode) {
|
|
81323
|
+
if (!r.website)
|
|
81324
|
+
continue;
|
|
81325
|
+
args.onProgress?.(`Checking ${r.name}'s booking setup\u2026`);
|
|
81326
|
+
const check2 = await (0, booking_detect_1.checkBookingOnSite)(r.website);
|
|
81327
|
+
bookingStatus = check2.verdict;
|
|
81328
|
+
bookingPlatform = check2.detail ?? null;
|
|
81329
|
+
if (!(0, booking_detect_1.isBookingProspect)(check2))
|
|
81330
|
+
continue;
|
|
81331
|
+
leadKind = "no_booking";
|
|
81332
|
+
} else if (r.website) {
|
|
81333
|
+
continue;
|
|
81334
|
+
}
|
|
81191
81335
|
db2.run(`INSERT INTO leads
|
|
81192
|
-
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status)
|
|
81193
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered')`, [
|
|
81336
|
+
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status, lead_kind, booking_status, booking_platform)
|
|
81337
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered', ?, ?, ?)`, [
|
|
81194
81338
|
r.place_id,
|
|
81195
81339
|
r.name,
|
|
81196
81340
|
r.type,
|
|
@@ -81204,11 +81348,23 @@ var require_lead_search = __commonJS({
|
|
|
81204
81348
|
r.lng,
|
|
81205
81349
|
r.score,
|
|
81206
81350
|
r.price_level,
|
|
81207
|
-
r.business_status
|
|
81351
|
+
r.business_status,
|
|
81352
|
+
leadKind,
|
|
81353
|
+
bookingStatus,
|
|
81354
|
+
bookingPlatform
|
|
81208
81355
|
]);
|
|
81209
81356
|
const row = db2.get("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
81210
81357
|
saved++;
|
|
81211
|
-
onLead({
|
|
81358
|
+
onLead({
|
|
81359
|
+
id: row?.id ?? 0,
|
|
81360
|
+
place_id: r.place_id,
|
|
81361
|
+
name: r.name,
|
|
81362
|
+
type: r.type,
|
|
81363
|
+
city: args.city,
|
|
81364
|
+
website: r.website,
|
|
81365
|
+
bookingStatus: bookingStatus ?? void 0,
|
|
81366
|
+
bookingDetail: bookingPlatform ?? void 0
|
|
81367
|
+
});
|
|
81212
81368
|
}
|
|
81213
81369
|
});
|
|
81214
81370
|
return { found, saved };
|
|
@@ -93031,12 +93187,13 @@ var require_chat_tools = __commonJS({
|
|
|
93031
93187
|
var builder_1 = require_builder();
|
|
93032
93188
|
var lead_search_1 = require_lead_search();
|
|
93033
93189
|
var places_1 = require_places();
|
|
93190
|
+
var booking_detect_1 = require_booking_detect();
|
|
93034
93191
|
var email_1 = require_email();
|
|
93035
93192
|
var email_harvest_1 = require_email_harvest();
|
|
93036
93193
|
exports2.TOOLS = [
|
|
93037
93194
|
{
|
|
93038
93195
|
name: "find_leads",
|
|
93039
|
-
description: "Search Google Places for local businesses that
|
|
93196
|
+
description: "Search Google Places for local business leads and save them. Two modes: mode='no_website' (default) finds businesses with NO website \u2014 sell them a demo site; mode='no_booking' finds businesses that HAVE a website but take bookings only via WhatsApp/phone (it fetches each site and checks for Fresha/Treatwell/OpenTable/Calendly etc) \u2014 sell those a standalone booking page. This is the lead-finding step \u2014 use it whenever the user wants to find/discover/search for new prospects. Give it a place (town, postcode, area or address) and one or more business categories. Costs ~\xA30.025 per category searched against the user's Google Places key. Businesses that already have a website are filtered out automatically, and duplicates are skipped.",
|
|
93040
93197
|
input_schema: {
|
|
93041
93198
|
type: "object",
|
|
93042
93199
|
properties: {
|
|
@@ -93056,6 +93213,11 @@ var require_chat_tools = __commonJS({
|
|
|
93056
93213
|
radiusMeters: {
|
|
93057
93214
|
type: "number",
|
|
93058
93215
|
description: "Search radius around the location in metres (default 10000 = 10km). Use a larger radius for big cities where central businesses mostly have websites; smaller (e.g. 3000) for a tight local search."
|
|
93216
|
+
},
|
|
93217
|
+
mode: {
|
|
93218
|
+
type: "string",
|
|
93219
|
+
enum: ["no_website", "no_booking"],
|
|
93220
|
+
description: "What kind of opportunity to hunt. 'no_website' (default) = businesses with no website at all \u2014 sell them a demo site. 'no_booking' = businesses that DO have a website but take bookings by WhatsApp/phone only \u2014 sell them a standalone booking page (sproutkit book <id> --standalone). Use 'no_booking' when the user asks for businesses without online booking, or wants booking-site customers."
|
|
93059
93221
|
}
|
|
93060
93222
|
},
|
|
93061
93223
|
required: ["location", "categories"]
|
|
@@ -93130,11 +93292,48 @@ var require_chat_tools = __commonJS({
|
|
|
93130
93292
|
type: "array",
|
|
93131
93293
|
items: { type: "number" },
|
|
93132
93294
|
description: "Specific lead ids to prepare. Omit to prepare every lead that has a site built but hasn't been contacted yet (status 'site_built')."
|
|
93295
|
+
},
|
|
93296
|
+
channel: {
|
|
93297
|
+
type: "string",
|
|
93298
|
+
enum: ["whatsapp", "sms"],
|
|
93299
|
+
description: "Messaging channel for leads that have a phone: 'whatsapp' opens WhatsApp, 'sms' opens the device's Messages app (Messages on macOS, Phone Link on Windows). Omit to use the saved preference."
|
|
93133
93300
|
}
|
|
93134
93301
|
}
|
|
93135
93302
|
},
|
|
93136
93303
|
requiresConfirmation: true
|
|
93137
93304
|
},
|
|
93305
|
+
{
|
|
93306
|
+
name: "booking_site_for_business",
|
|
93307
|
+
description: "ONE-SHOT: build a standalone booking site for a business the user NAMES, even if it isn't a lead yet. Give it the business name (include the town/area if known, e.g. 'Kings Barbers, Leeds'). It looks the business up on Google Places, reads their existing website to understand the business and check whether they already take bookings online, saves them as a lead, and deploys a booking page on its own URL for them to link from their site. Use this for requests like 'build a booking site for The Oak Barbers' when that business isn't in the pipeline. Costs a Places lookup + a Vercel deploy.",
|
|
93308
|
+
input_schema: {
|
|
93309
|
+
type: "object",
|
|
93310
|
+
properties: {
|
|
93311
|
+
business: {
|
|
93312
|
+
type: "string",
|
|
93313
|
+
description: "The business to look up \u2014 name plus town/area if known, e.g. 'Spice Garden takeaway Leeds'."
|
|
93314
|
+
},
|
|
93315
|
+
archetype: {
|
|
93316
|
+
type: "string",
|
|
93317
|
+
enum: ["slots", "reservation", "enquiry"],
|
|
93318
|
+
description: "Booking style: 'slots' = timed appointments, 'reservation' = table/party booking, 'enquiry' = a request form. Omit to auto-detect from the business type."
|
|
93319
|
+
},
|
|
93320
|
+
instructions: {
|
|
93321
|
+
type: "string",
|
|
93322
|
+
description: "Optional custom direction for the booking form (services, slot length, party size, copy)."
|
|
93323
|
+
},
|
|
93324
|
+
ownerEmail: {
|
|
93325
|
+
type: "string",
|
|
93326
|
+
description: "Optional owner email to notify on each booking."
|
|
93327
|
+
},
|
|
93328
|
+
force: {
|
|
93329
|
+
type: "boolean",
|
|
93330
|
+
description: "Build even if the business already appears to have online booking. Default false \u2014 normally we stop and say they already have it."
|
|
93331
|
+
}
|
|
93332
|
+
},
|
|
93333
|
+
required: ["business"]
|
|
93334
|
+
},
|
|
93335
|
+
requiresConfirmation: true
|
|
93336
|
+
},
|
|
93138
93337
|
{
|
|
93139
93338
|
name: "build_site",
|
|
93140
93339
|
description: "Generate and deploy a demo website for a lead that doesn't have one yet. This calls Claude (uses the user's Anthropic key), generates real React code with Google photos & reviews, and deploys to Vercel. Takes 30-60 seconds. Burns ~\xA30.02 in Anthropic credits + 1 Vercel deployment. Set includeCms=true to also install a self-serve /admin editor on the customer's own domain (Klaudius-style hand-off). Use regenerate_site if the lead already has a website.",
|
|
@@ -93327,6 +93526,14 @@ var require_chat_tools = __commonJS({
|
|
|
93327
93526
|
return `+44${p.slice(1)}`;
|
|
93328
93527
|
return p;
|
|
93329
93528
|
}
|
|
93529
|
+
function bookingOutreachTemplate(db2) {
|
|
93530
|
+
const row = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_template_booking"]);
|
|
93531
|
+
return row?.value ?? `Hi {name}, I saw you take bookings by {booking_how} \u2014 I've set up an online booking page you can link from your site: {site_url}
|
|
93532
|
+
|
|
93533
|
+
Customers pick a time themselves, you get an email for each booking. \xA3199 one-time, no monthly fee.
|
|
93534
|
+
|
|
93535
|
+
Want it live on your own domain?`;
|
|
93536
|
+
}
|
|
93330
93537
|
function outreachTemplate(db2) {
|
|
93331
93538
|
const row = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_template"]);
|
|
93332
93539
|
return row?.value ?? `Hi, we built {name} a free demo website: {site_url}
|
|
@@ -93395,7 +93602,7 @@ Interested?`;
|
|
|
93395
93602
|
const status = input.status;
|
|
93396
93603
|
const city = input.city;
|
|
93397
93604
|
const limit = Math.min(Number(input.limit ?? 25), 100);
|
|
93398
|
-
let sql = "SELECT id, name, status, type, city, phone, site_url FROM leads";
|
|
93605
|
+
let sql = "SELECT id, name, status, type, city, phone, site_url, website, lead_kind, booking_status, booking_platform, booking_url FROM leads";
|
|
93399
93606
|
const conds = [];
|
|
93400
93607
|
const params = [];
|
|
93401
93608
|
if (status) {
|
|
@@ -93472,7 +93679,7 @@ Interested?`;
|
|
|
93472
93679
|
}
|
|
93473
93680
|
case "prepare_outreach": {
|
|
93474
93681
|
const explicit = input.ids;
|
|
93475
|
-
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", []);
|
|
93682
|
+
const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, address, city, site_url, lead_kind, booking_platform, booking_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all("SELECT id, name, phone, email, address, city, site_url, lead_kind, booking_platform, booking_url FROM leads WHERE status = 'site_built' AND site_url IS NOT NULL AND site_url != '' ORDER BY score DESC NULLS LAST, id DESC", []);
|
|
93476
93683
|
const eligible = rows.filter((r) => r.site_url);
|
|
93477
93684
|
if (eligible.length === 0) {
|
|
93478
93685
|
return {
|
|
@@ -93481,14 +93688,17 @@ Interested?`;
|
|
|
93481
93688
|
};
|
|
93482
93689
|
}
|
|
93483
93690
|
const channelRow = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_channel"]);
|
|
93484
|
-
const
|
|
93691
|
+
const requested = String(input.channel ?? "").toLowerCase();
|
|
93692
|
+
const channel = requested === "sms" || requested === "whatsapp" ? requested : channelRow?.value || "whatsapp";
|
|
93485
93693
|
const template = outreachTemplate(db2);
|
|
93694
|
+
const bookingTemplate = bookingOutreachTemplate(db2);
|
|
93486
93695
|
const smtp = (0, email_1.smtpFromSettings)((k) => getDecryptedSetting(k));
|
|
93487
93696
|
const emailSubject = getDecryptedSetting("outreach_email_subject") || "A free demo website for {name}";
|
|
93488
93697
|
const prepared = [];
|
|
93489
93698
|
let harvested = 0;
|
|
93490
93699
|
for (const r of eligible) {
|
|
93491
|
-
const
|
|
93700
|
+
const isBookingLead = r.lead_kind === "no_booking";
|
|
93701
|
+
const message = (isBookingLead ? bookingTemplate : template).replace(/\{name\}/g, r.name).replace(/\{site_url\}/g, (isBookingLead ? r.booking_url : r.site_url) ?? r.site_url ?? "").replace(/\{booking_how\}/g, r.booking_platform || "WhatsApp/phone");
|
|
93492
93702
|
const e164 = toE164(r.phone ?? "");
|
|
93493
93703
|
if (!e164 && !r.email && smtp) {
|
|
93494
93704
|
ctx.emitProgress?.({
|
|
@@ -93618,6 +93828,84 @@ Interested?`;
|
|
|
93618
93828
|
});
|
|
93619
93829
|
return { ok: true, queued: ids.length, concurrency, snapshot };
|
|
93620
93830
|
}
|
|
93831
|
+
case "booking_site_for_business": {
|
|
93832
|
+
const query = String(input.business ?? "").trim();
|
|
93833
|
+
if (!query)
|
|
93834
|
+
return { ok: false, error: "Give me the business name (and town if you know it)." };
|
|
93835
|
+
const placesKey = getDecryptedSetting("places_key");
|
|
93836
|
+
if (!placesKey)
|
|
93837
|
+
throw new Error("Google Places API key not set");
|
|
93838
|
+
ctx.emitProgress?.({
|
|
93839
|
+
channel: "leads:searching",
|
|
93840
|
+
payload: { msg: `Looking up ${query}\u2026` }
|
|
93841
|
+
});
|
|
93842
|
+
const biz = await (0, places_1.findOneBusiness)({ apiKey: placesKey, query });
|
|
93843
|
+
if (!biz) {
|
|
93844
|
+
return { ok: false, error: `Couldn't find "${query}" on Google Places. Try adding the town, or check the spelling.` };
|
|
93845
|
+
}
|
|
93846
|
+
let check2 = { verdict: "unknown" };
|
|
93847
|
+
if (biz.website) {
|
|
93848
|
+
ctx.emitProgress?.({
|
|
93849
|
+
channel: "leads:searching",
|
|
93850
|
+
payload: { msg: `Reading ${biz.name}'s website\u2026` }
|
|
93851
|
+
});
|
|
93852
|
+
check2 = await (0, booking_detect_1.checkBookingOnSite)(biz.website);
|
|
93853
|
+
if (check2.verdict === "has_online_booking" && input.force !== true) {
|
|
93854
|
+
return {
|
|
93855
|
+
ok: false,
|
|
93856
|
+
error: `${biz.name} already takes bookings online via ${check2.detail}. Nothing to sell them \u2014 pass force=true to build anyway.`,
|
|
93857
|
+
business: biz.name,
|
|
93858
|
+
website: biz.website,
|
|
93859
|
+
bookingPlatform: check2.detail
|
|
93860
|
+
};
|
|
93861
|
+
}
|
|
93862
|
+
}
|
|
93863
|
+
const existing = db2.get("SELECT id FROM leads WHERE place_id = ?", [biz.place_id]);
|
|
93864
|
+
let leadId;
|
|
93865
|
+
if (existing) {
|
|
93866
|
+
leadId = existing.id;
|
|
93867
|
+
db2.run("UPDATE leads SET lead_kind = 'no_booking', booking_status = ?, booking_platform = ?, website = COALESCE(website, ?) WHERE id = ?", [check2.verdict, check2.detail ?? null, biz.website, leadId]);
|
|
93868
|
+
} else {
|
|
93869
|
+
db2.run(`INSERT INTO leads
|
|
93870
|
+
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status, lead_kind, booking_status, booking_platform)
|
|
93871
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered', 'no_booking', ?, ?)`, [
|
|
93872
|
+
biz.place_id,
|
|
93873
|
+
biz.name,
|
|
93874
|
+
biz.type,
|
|
93875
|
+
"",
|
|
93876
|
+
biz.address,
|
|
93877
|
+
biz.phone,
|
|
93878
|
+
biz.website,
|
|
93879
|
+
biz.rating,
|
|
93880
|
+
biz.review_count,
|
|
93881
|
+
biz.lat,
|
|
93882
|
+
biz.lng,
|
|
93883
|
+
biz.score,
|
|
93884
|
+
biz.price_level,
|
|
93885
|
+
biz.business_status,
|
|
93886
|
+
check2.verdict,
|
|
93887
|
+
check2.detail ?? null
|
|
93888
|
+
]);
|
|
93889
|
+
leadId = db2.get("SELECT id FROM leads WHERE place_id = ?", [biz.place_id]).id;
|
|
93890
|
+
}
|
|
93891
|
+
ctx.emitProgress?.({ channel: "leads:changed", payload: { reason: "booking_site_for_business" } });
|
|
93892
|
+
const result = await ctx.invokeIpc("bookings:provision-standalone", {
|
|
93893
|
+
leadId,
|
|
93894
|
+
instructions: input.instructions,
|
|
93895
|
+
archetypeOverride: input.archetype,
|
|
93896
|
+
ownerEmail: input.ownerEmail
|
|
93897
|
+
});
|
|
93898
|
+
db2.run("UPDATE leads SET booking_url = ? WHERE id = ?", [result.bookingUrl, leadId]);
|
|
93899
|
+
return {
|
|
93900
|
+
ok: true,
|
|
93901
|
+
id: leadId,
|
|
93902
|
+
business: biz.name,
|
|
93903
|
+
website: biz.website,
|
|
93904
|
+
currentBooking: check2.verdict === "messaging_only" ? check2.detail : check2.verdict,
|
|
93905
|
+
standalone: true,
|
|
93906
|
+
...result
|
|
93907
|
+
};
|
|
93908
|
+
}
|
|
93621
93909
|
case "add_booking_site": {
|
|
93622
93910
|
const id = Number(input.id);
|
|
93623
93911
|
const lead = db2.get("SELECT site_url FROM leads WHERE id = ?", [id]);
|
|
@@ -94661,7 +94949,7 @@ var require_package2 = __commonJS({
|
|
|
94661
94949
|
"package.json"(exports2, module2) {
|
|
94662
94950
|
module2.exports = {
|
|
94663
94951
|
name: "sproutkit-code",
|
|
94664
|
-
version: "1.
|
|
94952
|
+
version: "1.2.0",
|
|
94665
94953
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
94666
94954
|
bin: {
|
|
94667
94955
|
sproutkit: "./dist/index.js"
|
|
@@ -95656,7 +95944,8 @@ async function cmdFind(argv) {
|
|
|
95656
95944
|
const lead = payload.lead;
|
|
95657
95945
|
if (lead) {
|
|
95658
95946
|
streamed++;
|
|
95659
|
-
|
|
95947
|
+
const how = lead.bookingStatus === "messaging_only" ? c.amber(` \xB7 books via ${lead.bookingDetail ?? "messaging"}`) : lead.bookingStatus === "no_booking" ? c.amber(" \xB7 no online booking") : "";
|
|
95948
|
+
say(` ${c.green("+")} ${c.bold(lead.name)} ${c.dim(`#${lead.id} \xB7 ${lead.type}`)}${how}`);
|
|
95660
95949
|
}
|
|
95661
95950
|
}
|
|
95662
95951
|
};
|
|
@@ -95667,7 +95956,9 @@ async function cmdFind(argv) {
|
|
|
95667
95956
|
location,
|
|
95668
95957
|
categories: categories.length ? categories : [location],
|
|
95669
95958
|
maxResults: flags.max ? Number(flags.max) : void 0,
|
|
95670
|
-
radiusMeters: flags.radius ? Number(flags.radius) : void 0
|
|
95959
|
+
radiusMeters: flags.radius ? Number(flags.radius) : void 0,
|
|
95960
|
+
// --booking hunts businesses that HAVE a site but no online booking.
|
|
95961
|
+
mode: flags.booking ? "no_booking" : "no_website"
|
|
95671
95962
|
},
|
|
95672
95963
|
makeCtx({ json, onEvent })
|
|
95673
95964
|
);
|
|
@@ -95757,7 +96048,10 @@ async function cmdOutreach(argv) {
|
|
|
95757
96048
|
const ids = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
|
|
95758
96049
|
const result = await runTool(
|
|
95759
96050
|
"prepare_outreach",
|
|
95760
|
-
{
|
|
96051
|
+
{
|
|
96052
|
+
ids: ids.length ? ids : void 0,
|
|
96053
|
+
channel: flags.channel ? String(flags.channel).toLowerCase() : void 0
|
|
96054
|
+
},
|
|
95761
96055
|
makeCtx({ json })
|
|
95762
96056
|
);
|
|
95763
96057
|
if (json) {
|
|
@@ -95830,6 +96124,29 @@ async function cmdPipeline(argv) {
|
|
|
95830
96124
|
const queue = [...targets];
|
|
95831
96125
|
async function runOne(t) {
|
|
95832
96126
|
const setRow = (state, detail) => board?.update(rowFor(t, state, detail));
|
|
96127
|
+
if (t.lead_kind === "no_booking") {
|
|
96128
|
+
setRow("building", "building booking site\u2026");
|
|
96129
|
+
try {
|
|
96130
|
+
const booked = await runTool(
|
|
96131
|
+
"add_booking_site",
|
|
96132
|
+
{ id: t.id, standalone: true },
|
|
96133
|
+
makeCtx({ json: true })
|
|
96134
|
+
);
|
|
96135
|
+
if (booked && booked.ok === false) {
|
|
96136
|
+
setRow("failed", booked.error ?? "booking build failed");
|
|
96137
|
+
results.push({ id: t.id, name: t.name, ok: false, error: booked.error });
|
|
96138
|
+
return;
|
|
96139
|
+
}
|
|
96140
|
+
setRow("building", "preparing outreach\u2026");
|
|
96141
|
+
await runTool("prepare_outreach", { ids: [t.id] }, makeCtx({ json: true }));
|
|
96142
|
+
setRow("built", booked.bookingUrl ?? "booking site live");
|
|
96143
|
+
results.push({ id: t.id, name: t.name, ok: true, url: booked.bookingUrl });
|
|
96144
|
+
} catch (e) {
|
|
96145
|
+
setRow("failed", e.message.slice(0, 60));
|
|
96146
|
+
results.push({ id: t.id, name: t.name, ok: false, error: e.message });
|
|
96147
|
+
}
|
|
96148
|
+
return;
|
|
96149
|
+
}
|
|
95833
96150
|
setRow("building", "building site\u2026");
|
|
95834
96151
|
try {
|
|
95835
96152
|
const built = await runTool(
|
|
@@ -95958,9 +96275,42 @@ async function cmdCms(argv) {
|
|
|
95958
96275
|
async function cmdBook(argv) {
|
|
95959
96276
|
const { flags, rest } = parseArgs(argv);
|
|
95960
96277
|
const json = !!flags.json;
|
|
96278
|
+
const positional = rest.join(" ").trim();
|
|
95961
96279
|
const id = Number(flags.id ?? rest[0]);
|
|
96280
|
+
if (!flags.id && positional && Number.isNaN(Number(rest[0]))) {
|
|
96281
|
+
const status2 = json ? void 0 : new Status();
|
|
96282
|
+
if (status2) status2.start(`Building a booking site for ${positional}`);
|
|
96283
|
+
try {
|
|
96284
|
+
const r = await runTool(
|
|
96285
|
+
"booking_site_for_business",
|
|
96286
|
+
{
|
|
96287
|
+
business: positional,
|
|
96288
|
+
archetype: flags.archetype ? String(flags.archetype) : void 0,
|
|
96289
|
+
instructions: flags.instructions ? String(flags.instructions) : void 0,
|
|
96290
|
+
ownerEmail: flags.email ? String(flags.email) : void 0,
|
|
96291
|
+
force: !!flags.force
|
|
96292
|
+
},
|
|
96293
|
+
makeCtx({ json, status: status2 })
|
|
96294
|
+
);
|
|
96295
|
+
if (r && r.ok === false) {
|
|
96296
|
+
if (status2) status2.done(false, r.error ?? "Couldn't build it");
|
|
96297
|
+
if (json) emitJson(r);
|
|
96298
|
+
process.exitCode = 1;
|
|
96299
|
+
return;
|
|
96300
|
+
}
|
|
96301
|
+
if (status2) status2.done(true, `${r.business} \u2014 ${r.bookingUrl}`);
|
|
96302
|
+
autoSync();
|
|
96303
|
+
if (json) emitJson(r);
|
|
96304
|
+
else if (r.currentBooking) say(c.dim(` They currently take bookings via ${r.currentBooking}.`));
|
|
96305
|
+
} catch (e) {
|
|
96306
|
+
if (status2) status2.done(false, "Failed");
|
|
96307
|
+
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
96308
|
+
process.exitCode = 1;
|
|
96309
|
+
}
|
|
96310
|
+
return;
|
|
96311
|
+
}
|
|
95962
96312
|
if (!id || Number.isNaN(id)) {
|
|
95963
|
-
const err = 'Usage: sproutkit book <leadId> [--standalone] [--
|
|
96313
|
+
const err = 'Usage: sproutkit book <leadId|"Business Name, Town"> [--standalone] [--archetype \u2026] [--email owner@x.com]';
|
|
95964
96314
|
json ? emitJson({ ok: false, error: err }) : say(c.red(" " + err));
|
|
95965
96315
|
process.exitCode = 1;
|
|
95966
96316
|
return;
|
|
@@ -96097,11 +96447,13 @@ When a request is missing information the command needs, ask a SHORT, STRUCTURED
|
|
|
96097
96447
|
|
|
96098
96448
|
COMMANDS:
|
|
96099
96449
|
- sproutkit find "<area>" --categories "<type>,<type>" --json \u2014 find website-less leads
|
|
96450
|
+
- sproutkit find "<area>" --categories "\u2026" --booking --json \u2014 find businesses that HAVE a site but take bookings via WhatsApp/phone only (booking-page prospects)
|
|
96100
96451
|
- sproutkit list --json [--status <s>] [--city <c>] \u2014 list leads (get ids)
|
|
96101
96452
|
- sproutkit build <id> --json [--instructions "..."] [--cms] [--multipage] \u2014 build a demo site (--multipage = real Home/Services/About/Contact pages, not one scroll)
|
|
96102
96453
|
- sproutkit build-all --json [--ids 3,5] [--concurrency 3] \u2014 build many at once
|
|
96103
96454
|
- sproutkit regenerate <id> --instructions "..." --json \u2014 rebuild an existing site with changes
|
|
96104
96455
|
- sproutkit cms <id> --json \u2014 add the editable CMS on top of the site we built them
|
|
96456
|
+
- sproutkit book "<Business Name, Town>" --json \u2014 ONE-SHOT: look up any business (even not a lead), read their site, build them a standalone booking page
|
|
96105
96457
|
- sproutkit book <id> --json [--standalone] [--instructions "..."] [--archetype slots|reservation|enquiry] [--email x@y.com] \u2014 booking site (--standalone = its own URL for clients who already have a website)
|
|
96106
96458
|
- sproutkit outreach --json [--ids 3,5] \u2014 prepare outreach links
|
|
96107
96459
|
- sproutkit pipeline x3 --json [--book] [--limit 10] \u2014 full pipeline (build\u2192booking\u2192outreach) for many clients concurrently
|
|
@@ -96281,6 +96633,62 @@ async function main() {
|
|
|
96281
96633
|
}
|
|
96282
96634
|
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach" || arg === "pipeline") {
|
|
96283
96635
|
const subArgs = process.argv.slice(3);
|
|
96636
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
96637
|
+
const USAGE = {
|
|
96638
|
+
find: [
|
|
96639
|
+
'sproutkit find "<area>" --categories "barber shop,cafe"',
|
|
96640
|
+
" --booking find businesses that HAVE a site but no online booking",
|
|
96641
|
+
" --max <n> max leads to save (default 25)",
|
|
96642
|
+
" --radius <m> search radius in metres (default 10000)",
|
|
96643
|
+
" --json machine-readable output"
|
|
96644
|
+
],
|
|
96645
|
+
list: [
|
|
96646
|
+
"sproutkit list [--status <s>] [--city <c>] [--limit <n>] [--json]",
|
|
96647
|
+
" statuses: discovered, site_built, contacted, replied, won, dead"
|
|
96648
|
+
],
|
|
96649
|
+
build: [
|
|
96650
|
+
"sproutkit build <leadId> [options]",
|
|
96651
|
+
' --instructions "\u2026" tailor the design/content',
|
|
96652
|
+
" --cms include the editable CMS",
|
|
96653
|
+
" --multipage real Home/Services/About/Contact pages",
|
|
96654
|
+
" --skip-qa skip the post-build review (faster, lower quality)"
|
|
96655
|
+
],
|
|
96656
|
+
"build-all": [
|
|
96657
|
+
"sproutkit build-all [--ids 3,5] [--concurrency 3] [--json]",
|
|
96658
|
+
" builds every unbuilt lead unless --ids is given; press q to stop"
|
|
96659
|
+
],
|
|
96660
|
+
regenerate: [
|
|
96661
|
+
'sproutkit regenerate <leadId> --instructions "what to change"',
|
|
96662
|
+
" --multipage rebuild as a multipage site"
|
|
96663
|
+
],
|
|
96664
|
+
cms: ["sproutkit cms <leadId>", " adds the editable CMS to a site we already built"],
|
|
96665
|
+
book: [
|
|
96666
|
+
'sproutkit book <leadId | "Business Name, Town"> [options]',
|
|
96667
|
+
" naming a business that isn't a lead yet looks it up, reads their site,",
|
|
96668
|
+
" and builds them a standalone booking page in one shot",
|
|
96669
|
+
" --standalone booking site on its OWN url (client already has a site)",
|
|
96670
|
+
" --archetype <type> slots | reservation | enquiry",
|
|
96671
|
+
' --instructions "\u2026" e.g. "45-min slots, add beard trim \xA312"',
|
|
96672
|
+
" --email <addr> owner email for booking notifications"
|
|
96673
|
+
],
|
|
96674
|
+
outreach: [
|
|
96675
|
+
"sproutkit outreach [--ids 3,5] [--channel sms|whatsapp] [--json]",
|
|
96676
|
+
" Per lead: phone -> a messaging link you open; no phone -> emailed (needs SMTP).",
|
|
96677
|
+
" Moves reached leads to Contacted. Targets every lead with a built site",
|
|
96678
|
+
" unless --ids is given."
|
|
96679
|
+
],
|
|
96680
|
+
pipeline: [
|
|
96681
|
+
"sproutkit pipeline x3 [--book] [--limit 10] [--ids 3,5] [--json]",
|
|
96682
|
+
" build -> (booking) -> outreach for many clients concurrently (max 4)",
|
|
96683
|
+
" press q to stop after in-flight clients finish"
|
|
96684
|
+
]
|
|
96685
|
+
};
|
|
96686
|
+
say();
|
|
96687
|
+
for (const line of USAGE[arg] ?? [`sproutkit ${arg}`]) say(" " + line);
|
|
96688
|
+
say();
|
|
96689
|
+
closeTty();
|
|
96690
|
+
return;
|
|
96691
|
+
}
|
|
96284
96692
|
const lic2 = await check();
|
|
96285
96693
|
if (lic2.status !== "active") {
|
|
96286
96694
|
const msg = lic2.status === "offline_grace_expired" ? "Licence check offline and grace expired \u2014 reconnect and retry." : "No active licence. Run 'sproutkit setup' first.";
|
|
@@ -96314,7 +96722,7 @@ async function main() {
|
|
|
96314
96722
|
say(` ${c.bold("sproutkit build-all")} build many at once (--ids, --concurrency 3)`);
|
|
96315
96723
|
say(` ${c.bold("sproutkit regenerate <id>")} rebuild a site (--instructions "\u2026")`);
|
|
96316
96724
|
say(` ${c.bold("sproutkit cms <id>")} add the editable CMS to a built site`);
|
|
96317
|
-
say(` ${c.bold("sproutkit book <id>")}
|
|
96725
|
+
say(` ${c.bold("sproutkit book <id|name>")} booking site (--standalone, or name any business)`);
|
|
96318
96726
|
say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
|
|
96319
96727
|
say(` ${c.bold("sproutkit pipeline x3")} build+outreach many clients concurrently`);
|
|
96320
96728
|
say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
|