sproutkit-code 0.5.1 → 0.7.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 +295 -21
- package/dist/skill/SKILL.md +11 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -79482,6 +79482,7 @@ var require_builder = __commonJS({
|
|
|
79482
79482
|
exports2.buildAndDeploySite = buildAndDeploySite;
|
|
79483
79483
|
exports2.redeployCmsOnly = redeployCmsOnly;
|
|
79484
79484
|
exports2.deployBookingOnly = deployBookingOnly2;
|
|
79485
|
+
exports2.deployStandaloneBooking = deployStandaloneBooking2;
|
|
79485
79486
|
var sdk_1 = __importDefault(require_sdk());
|
|
79486
79487
|
var axios_1 = __importDefault(require_axios());
|
|
79487
79488
|
var runtime_1 = require_runtime();
|
|
@@ -80372,6 +80373,63 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
80372
80373
|
onProgress(`Live at ${publicUrl}`);
|
|
80373
80374
|
return { url: publicUrl, bookingUrl: `${publicUrl}/book` };
|
|
80374
80375
|
}
|
|
80376
|
+
async function deployStandaloneBooking2(args) {
|
|
80377
|
+
const { lead, vercelToken, blobToken, resendKey, ownerEmail, bookingConfig, onProgress } = args;
|
|
80378
|
+
const slug = lead.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 26);
|
|
80379
|
+
const projectName = `sprout-book-${lead.id}-${slug}`;
|
|
80380
|
+
onProgress("Seeding booking config\u2026");
|
|
80381
|
+
try {
|
|
80382
|
+
const { put } = await Promise.resolve().then(() => __importStar(require_dist5()));
|
|
80383
|
+
await put("booking-config.json", JSON.stringify(bookingConfig), {
|
|
80384
|
+
access: "public",
|
|
80385
|
+
addRandomSuffix: false,
|
|
80386
|
+
allowOverwrite: true,
|
|
80387
|
+
contentType: "application/json",
|
|
80388
|
+
token: blobToken
|
|
80389
|
+
});
|
|
80390
|
+
} catch (e) {
|
|
80391
|
+
throw new Error(`Failed to seed booking config: ${e.message}`);
|
|
80392
|
+
}
|
|
80393
|
+
onProgress("Setting booking env vars\u2026");
|
|
80394
|
+
await setProjectEnv(projectName, vercelToken, {
|
|
80395
|
+
RESEND_API_KEY: resendKey,
|
|
80396
|
+
OWNER_EMAIL: ownerEmail,
|
|
80397
|
+
BUSINESS_NAME: lead.name,
|
|
80398
|
+
BLOB_READ_WRITE_TOKEN: blobToken
|
|
80399
|
+
});
|
|
80400
|
+
const files = [
|
|
80401
|
+
{ file: "index.html", data: booking_runtime_1.BOOK_INDEX_HTML, encoding: "utf-8" },
|
|
80402
|
+
{ file: "app.js", data: booking_runtime_1.BOOK_APP_JS, encoding: "utf-8" },
|
|
80403
|
+
{ file: "api/bookings.js", data: booking_runtime_1.API_BOOKINGS_JS, encoding: "utf-8" },
|
|
80404
|
+
{ file: "api/booking-config.js", data: booking_runtime_1.API_BOOKING_CONFIG_JS, encoding: "utf-8" },
|
|
80405
|
+
{
|
|
80406
|
+
file: "package.json",
|
|
80407
|
+
data: JSON.stringify({
|
|
80408
|
+
name: "sprout-booking",
|
|
80409
|
+
private: true,
|
|
80410
|
+
version: "0.1.0",
|
|
80411
|
+
type: "module",
|
|
80412
|
+
dependencies: { "@vercel/blob": "^0.23.4" }
|
|
80413
|
+
}),
|
|
80414
|
+
encoding: "utf-8"
|
|
80415
|
+
}
|
|
80416
|
+
];
|
|
80417
|
+
files[0] = {
|
|
80418
|
+
file: "index.html",
|
|
80419
|
+
data: booking_runtime_1.BOOK_INDEX_HTML.replace(/\/book\/app\.js/g, "/app.js"),
|
|
80420
|
+
encoding: "utf-8"
|
|
80421
|
+
};
|
|
80422
|
+
onProgress("Deploying booking site\u2026");
|
|
80423
|
+
const res = await postVercelDeployment({ name: projectName, files, target: "production" }, vercelToken, onProgress);
|
|
80424
|
+
await axios_1.default.patch(`https://api.vercel.com/v9/projects/${res.data.name}`, { ssoProtection: null }, { headers: { Authorization: `Bearer ${vercelToken}` } }).catch(() => null);
|
|
80425
|
+
const publicUrl = await stableProjectUrl({
|
|
80426
|
+
projectName,
|
|
80427
|
+
vercelToken,
|
|
80428
|
+
fallbackUrl: `https://${res.data.url}`
|
|
80429
|
+
});
|
|
80430
|
+
onProgress(`Live at ${publicUrl}`);
|
|
80431
|
+
return { url: publicUrl, bookingUrl: publicUrl };
|
|
80432
|
+
}
|
|
80375
80433
|
function hasBookLink(code) {
|
|
80376
80434
|
return /(href\s*=\s*["'`{]*\s*["'`]?\/book\/?["'`}]?)|(location(?:\.href)?\s*=\s*["'`]\/book["'`])/i.test(code);
|
|
80377
80435
|
}
|
|
@@ -80585,6 +80643,7 @@ var require_places = __commonJS({
|
|
|
80585
80643
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80586
80644
|
exports2.PLACES_COST_PER_REQUEST_GBP = void 0;
|
|
80587
80645
|
exports2.scoreLead = scoreLead;
|
|
80646
|
+
exports2.searchPlacesStream = searchPlacesStream;
|
|
80588
80647
|
exports2.searchPlaces = searchPlaces;
|
|
80589
80648
|
exports2.geocodeForward = geocodeForward;
|
|
80590
80649
|
exports2.geocodeReverse = geocodeReverse;
|
|
@@ -80642,6 +80701,53 @@ var require_places = __commonJS({
|
|
|
80642
80701
|
return { type: args.type, places: [] };
|
|
80643
80702
|
}
|
|
80644
80703
|
}
|
|
80704
|
+
function processRawPlaces(type, places, center, radiusMeters) {
|
|
80705
|
+
const out = [];
|
|
80706
|
+
for (const p of places) {
|
|
80707
|
+
if (p.websiteUri)
|
|
80708
|
+
continue;
|
|
80709
|
+
if (center && radiusMeters && p.location) {
|
|
80710
|
+
const d = haversineMeters(center.lat, center.lng, p.location.latitude, p.location.longitude);
|
|
80711
|
+
if (d > radiusMeters)
|
|
80712
|
+
continue;
|
|
80713
|
+
}
|
|
80714
|
+
const phone = p.nationalPhoneNumber ?? "";
|
|
80715
|
+
const rating = p.rating ?? null;
|
|
80716
|
+
const review_count = p.userRatingCount ?? null;
|
|
80717
|
+
const price_level = p.priceLevel ?? null;
|
|
80718
|
+
const business_status = p.businessStatus ?? null;
|
|
80719
|
+
const has_hours = !!p.regularOpeningHours;
|
|
80720
|
+
out.push({
|
|
80721
|
+
place_id: p.id,
|
|
80722
|
+
name: p.displayName?.text ?? "Unknown",
|
|
80723
|
+
type,
|
|
80724
|
+
address: p.formattedAddress ?? "",
|
|
80725
|
+
phone,
|
|
80726
|
+
website: p.websiteUri ?? null,
|
|
80727
|
+
rating,
|
|
80728
|
+
review_count,
|
|
80729
|
+
lat: p.location?.latitude ?? null,
|
|
80730
|
+
lng: p.location?.longitude ?? null,
|
|
80731
|
+
price_level,
|
|
80732
|
+
business_status,
|
|
80733
|
+
has_hours,
|
|
80734
|
+
score: scoreLead({ rating, review_count, price_level, phone, has_hours, business_status })
|
|
80735
|
+
});
|
|
80736
|
+
}
|
|
80737
|
+
return out;
|
|
80738
|
+
}
|
|
80739
|
+
async function searchPlacesStream(args, onBatch) {
|
|
80740
|
+
await Promise.all(args.types.map(async (type) => {
|
|
80741
|
+
const { places } = await searchOneType({
|
|
80742
|
+
apiKey: args.apiKey,
|
|
80743
|
+
city: args.city,
|
|
80744
|
+
type,
|
|
80745
|
+
center: args.center,
|
|
80746
|
+
radiusMeters: args.radiusMeters
|
|
80747
|
+
});
|
|
80748
|
+
await onBatch(processRawPlaces(type, places, args.center, args.radiusMeters));
|
|
80749
|
+
}));
|
|
80750
|
+
}
|
|
80645
80751
|
async function searchPlaces(args) {
|
|
80646
80752
|
const batches = await Promise.all(args.types.map((type) => searchOneType({
|
|
80647
80753
|
apiKey: args.apiKey,
|
|
@@ -80736,9 +80842,58 @@ var require_lead_search = __commonJS({
|
|
|
80736
80842
|
"../desktop/dist/main/lib/lead-search.js"(exports2) {
|
|
80737
80843
|
"use strict";
|
|
80738
80844
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80845
|
+
exports2.searchAndSaveLeadsStream = searchAndSaveLeadsStream;
|
|
80739
80846
|
exports2.searchAndSaveLeads = searchAndSaveLeads;
|
|
80740
80847
|
var db_1 = require_db();
|
|
80741
80848
|
var places_1 = require_places();
|
|
80849
|
+
async function searchAndSaveLeadsStream(args, onLead) {
|
|
80850
|
+
const db2 = (0, db_1.getDb)();
|
|
80851
|
+
const cap = args.maxResults ?? 50;
|
|
80852
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80853
|
+
let saved = 0;
|
|
80854
|
+
let found = 0;
|
|
80855
|
+
await (0, places_1.searchPlacesStream)({
|
|
80856
|
+
apiKey: args.placesKey,
|
|
80857
|
+
city: args.city,
|
|
80858
|
+
types: args.types,
|
|
80859
|
+
center: args.center,
|
|
80860
|
+
radiusMeters: args.radiusMeters
|
|
80861
|
+
}, (batch) => {
|
|
80862
|
+
for (const r of batch) {
|
|
80863
|
+
found++;
|
|
80864
|
+
if (saved >= cap)
|
|
80865
|
+
continue;
|
|
80866
|
+
if (seen.has(r.place_id))
|
|
80867
|
+
continue;
|
|
80868
|
+
seen.add(r.place_id);
|
|
80869
|
+
const existing = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
80870
|
+
if (existing.length > 0)
|
|
80871
|
+
continue;
|
|
80872
|
+
db2.run(`INSERT INTO leads
|
|
80873
|
+
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status)
|
|
80874
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered')`, [
|
|
80875
|
+
r.place_id,
|
|
80876
|
+
r.name,
|
|
80877
|
+
r.type,
|
|
80878
|
+
args.city,
|
|
80879
|
+
r.address,
|
|
80880
|
+
r.phone,
|
|
80881
|
+
r.website,
|
|
80882
|
+
r.rating,
|
|
80883
|
+
r.review_count,
|
|
80884
|
+
r.lat,
|
|
80885
|
+
r.lng,
|
|
80886
|
+
r.score,
|
|
80887
|
+
r.price_level,
|
|
80888
|
+
r.business_status
|
|
80889
|
+
]);
|
|
80890
|
+
const row = db2.get("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
80891
|
+
saved++;
|
|
80892
|
+
onLead({ id: row?.id ?? 0, place_id: r.place_id, name: r.name, type: r.type, city: args.city });
|
|
80893
|
+
}
|
|
80894
|
+
});
|
|
80895
|
+
return { found, saved };
|
|
80896
|
+
}
|
|
80742
80897
|
async function searchAndSaveLeads(args) {
|
|
80743
80898
|
const db2 = (0, db_1.getDb)();
|
|
80744
80899
|
const results = await (0, places_1.searchPlaces)({
|
|
@@ -80950,11 +81105,15 @@ var require_chat_tools = __commonJS({
|
|
|
80950
81105
|
},
|
|
80951
81106
|
{
|
|
80952
81107
|
name: "add_booking_site",
|
|
80953
|
-
description: "
|
|
81108
|
+
description: "Create an online booking / reservation form for a lead. Sproutkit designs a form suited to the business \u2014 appointment slots for barbers/salons, table reservations for restaurants, or an enquiry form. TWO modes: (1) default \u2014 deploy it onto a website Sproutkit ALREADY built for this lead, on the same domain ('add a booking system to The Porch'); (2) standalone (set standalone=true) \u2014 a self-contained booking site with its OWN url, for a client who already has their own website and just needs a booking page to link to ('build a booking site for this client'). Pass `instructions` to customise (services, slot length, party size, copy) and `archetype` to force a style. Costs a Claude call + a Vercel deploy.",
|
|
80954
81109
|
input_schema: {
|
|
80955
81110
|
type: "object",
|
|
80956
81111
|
properties: {
|
|
80957
|
-
id: { type: "number", description: "The lead's id.
|
|
81112
|
+
id: { type: "number", description: "The lead's id." },
|
|
81113
|
+
standalone: {
|
|
81114
|
+
type: "boolean",
|
|
81115
|
+
description: "true = build a standalone booking site with its own URL (no Sproutkit-built website required \u2014 for clients who already have their own site). false/omit = add booking onto a site Sproutkit already built for this lead."
|
|
81116
|
+
},
|
|
80958
81117
|
instructions: {
|
|
80959
81118
|
type: "string",
|
|
80960
81119
|
description: "Optional custom direction for the booking form, in plain English. e.g. '45-minute slots, add a beard trim at \xA312', 'tables up to 12 people, ask for the occasion', 'just a simple enquiry form'. Shapes the deployed form."
|
|
@@ -81106,15 +81265,20 @@ Interested?`;
|
|
|
81106
81265
|
});
|
|
81107
81266
|
const matches = await (0, places_1.geocodeForward)(location, 1);
|
|
81108
81267
|
const place = matches[0];
|
|
81109
|
-
const result = await (0, lead_search_1.
|
|
81110
|
-
|
|
81111
|
-
|
|
81112
|
-
|
|
81113
|
-
|
|
81114
|
-
|
|
81115
|
-
|
|
81116
|
-
|
|
81117
|
-
|
|
81268
|
+
const result = await (0, lead_search_1.searchAndSaveLeadsStream)(
|
|
81269
|
+
{
|
|
81270
|
+
placesKey,
|
|
81271
|
+
// searchPlaces uses `city` as the text query, so keep the user's words.
|
|
81272
|
+
city: location,
|
|
81273
|
+
types: categories,
|
|
81274
|
+
maxResults: Math.min(Number(input.maxResults ?? 25), 100),
|
|
81275
|
+
center: place ? { lat: place.lat, lng: place.lng } : void 0,
|
|
81276
|
+
radiusMeters: Number(input.radiusMeters ?? 3e3)
|
|
81277
|
+
},
|
|
81278
|
+
// Emit each lead the instant it's saved, so the UI/CLI streams results
|
|
81279
|
+
// in rather than waiting for every category to finish.
|
|
81280
|
+
(lead) => ctx.emitProgress?.({ channel: "leads:found", payload: { lead } })
|
|
81281
|
+
);
|
|
81118
81282
|
ctx.emitProgress?.({
|
|
81119
81283
|
channel: "leads:changed",
|
|
81120
81284
|
payload: { reason: "find_leads", ...result }
|
|
@@ -81295,10 +81459,20 @@ Interested?`;
|
|
|
81295
81459
|
const lead = db2.get("SELECT site_url FROM leads WHERE id = ?", [id]);
|
|
81296
81460
|
if (!lead)
|
|
81297
81461
|
throw new Error(`Lead #${id} not found`);
|
|
81462
|
+
const standalone = input.standalone === true;
|
|
81463
|
+
if (standalone) {
|
|
81464
|
+
const result2 = await ctx.invokeIpc("bookings:provision-standalone", {
|
|
81465
|
+
leadId: id,
|
|
81466
|
+
instructions: input.instructions,
|
|
81467
|
+
archetypeOverride: input.archetype,
|
|
81468
|
+
ownerEmail: input.ownerEmail
|
|
81469
|
+
});
|
|
81470
|
+
return { ok: true, id, standalone: true, ...result2 };
|
|
81471
|
+
}
|
|
81298
81472
|
if (!lead.site_url) {
|
|
81299
81473
|
return {
|
|
81300
81474
|
ok: false,
|
|
81301
|
-
error: "This lead has no website
|
|
81475
|
+
error: "This lead has no Sproutkit-built website. Either build the site first, or set standalone=true to create a booking site with its own URL (for clients who already have their own website)."
|
|
81302
81476
|
};
|
|
81303
81477
|
}
|
|
81304
81478
|
const result = await ctx.invokeIpc("bookings:provision", {
|
|
@@ -82282,7 +82456,7 @@ var require_package = __commonJS({
|
|
|
82282
82456
|
"package.json"(exports2, module2) {
|
|
82283
82457
|
module2.exports = {
|
|
82284
82458
|
name: "sproutkit-code",
|
|
82285
|
-
version: "0.
|
|
82459
|
+
version: "0.7.0",
|
|
82286
82460
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
82287
82461
|
bin: {
|
|
82288
82462
|
sproutkit: "./dist/index.js"
|
|
@@ -82339,6 +82513,7 @@ var startAutoJob = autoMode.startAutoJob;
|
|
|
82339
82513
|
var provisionCmsForLead = cms.provisionCmsForLead;
|
|
82340
82514
|
var generatePreset = bookingPreset.generatePreset;
|
|
82341
82515
|
var deployBookingOnly = builder.deployBookingOnly;
|
|
82516
|
+
var deployStandaloneBooking = builder.deployStandaloneBooking;
|
|
82342
82517
|
var refreshModels = models.refreshModels;
|
|
82343
82518
|
var refreshIfStale = models.refreshIfStale;
|
|
82344
82519
|
var listCachedModels = models.listCachedModels;
|
|
@@ -82886,6 +83061,46 @@ function makeInvokeIpc(emit) {
|
|
|
82886
83061
|
emit("bookings:done", { leadId: p.leadId, ...result });
|
|
82887
83062
|
return result;
|
|
82888
83063
|
}
|
|
83064
|
+
if (channel === "bookings:provision-standalone") {
|
|
83065
|
+
const p = payload;
|
|
83066
|
+
const db2 = getDb();
|
|
83067
|
+
const lead = db2.get("SELECT * FROM leads WHERE id = ?", [p.leadId]);
|
|
83068
|
+
if (!lead) throw new Error("Lead not found");
|
|
83069
|
+
const vercelToken = getSetting("vercel_token");
|
|
83070
|
+
const blobToken = getSetting("blob_token");
|
|
83071
|
+
const resendKey = getSetting("resend_key");
|
|
83072
|
+
if (!vercelToken) throw new Error("Vercel token not set");
|
|
83073
|
+
if (!blobToken) throw new Error("Vercel Blob token not set");
|
|
83074
|
+
emit("bookings:progress", { leadId: p.leadId, msg: "Designing the booking form\u2026" });
|
|
83075
|
+
const leadInfo = lead;
|
|
83076
|
+
const config = await generatePreset({
|
|
83077
|
+
anthropicKey: getSetting("anthropic_key") ?? "",
|
|
83078
|
+
lead: { name: leadInfo.name, type: leadInfo.type, city: leadInfo.city },
|
|
83079
|
+
instructions: p.instructions,
|
|
83080
|
+
archetypeOverride: p.archetypeOverride
|
|
83081
|
+
});
|
|
83082
|
+
const result = await deployStandaloneBooking({
|
|
83083
|
+
lead,
|
|
83084
|
+
vercelToken,
|
|
83085
|
+
blobToken,
|
|
83086
|
+
resendKey: resendKey || "",
|
|
83087
|
+
ownerEmail: p.ownerEmail ?? "",
|
|
83088
|
+
bookingConfig: config,
|
|
83089
|
+
onProgress: (msg) => emit("bookings:progress", { leadId: p.leadId, msg })
|
|
83090
|
+
});
|
|
83091
|
+
db2.run(
|
|
83092
|
+
"UPDATE leads SET booking_url = ?, owner_email = ?, booking_archetype = ?, booking_config = ? WHERE id = ?",
|
|
83093
|
+
[
|
|
83094
|
+
result.bookingUrl,
|
|
83095
|
+
p.ownerEmail ?? "",
|
|
83096
|
+
config.archetype,
|
|
83097
|
+
JSON.stringify(config),
|
|
83098
|
+
p.leadId
|
|
83099
|
+
]
|
|
83100
|
+
);
|
|
83101
|
+
emit("bookings:done", { leadId: p.leadId, ...result });
|
|
83102
|
+
return result;
|
|
83103
|
+
}
|
|
82889
83104
|
throw new Error(`channel not supported in CLI: ${channel}`);
|
|
82890
83105
|
};
|
|
82891
83106
|
}
|
|
@@ -83244,6 +83459,7 @@ async function runModels() {
|
|
|
83244
83459
|
// src/commands.ts
|
|
83245
83460
|
function makeCtx(opts) {
|
|
83246
83461
|
const emit = (channel, payload) => {
|
|
83462
|
+
opts.onEvent?.(channel, payload);
|
|
83247
83463
|
if (opts.json) return;
|
|
83248
83464
|
const p = payload;
|
|
83249
83465
|
if (p?.msg && opts.status) opts.status.update(p.msg);
|
|
@@ -83287,8 +83503,18 @@ async function cmdFind(argv) {
|
|
|
83287
83503
|
return;
|
|
83288
83504
|
}
|
|
83289
83505
|
const categories = String(flags.categories ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
83290
|
-
|
|
83291
|
-
|
|
83506
|
+
if (!json) say(c.dim(` Searching ${location}${categories.length ? ` for ${categories.join(", ")}` : ""}\u2026`));
|
|
83507
|
+
let streamed = 0;
|
|
83508
|
+
const onEvent = (channel, payload) => {
|
|
83509
|
+
if (json) return;
|
|
83510
|
+
if (channel === "leads:found") {
|
|
83511
|
+
const lead = payload.lead;
|
|
83512
|
+
if (lead) {
|
|
83513
|
+
streamed++;
|
|
83514
|
+
say(` ${c.green("+")} ${c.bold(lead.name)} ${c.dim(`#${lead.id} \xB7 ${lead.type}`)}`);
|
|
83515
|
+
}
|
|
83516
|
+
}
|
|
83517
|
+
};
|
|
83292
83518
|
try {
|
|
83293
83519
|
const result = await runTool(
|
|
83294
83520
|
"find_leads",
|
|
@@ -83298,14 +83524,18 @@ async function cmdFind(argv) {
|
|
|
83298
83524
|
maxResults: flags.max ? Number(flags.max) : void 0,
|
|
83299
83525
|
radiusMeters: flags.radius ? Number(flags.radius) : void 0
|
|
83300
83526
|
},
|
|
83301
|
-
makeCtx({ json,
|
|
83527
|
+
makeCtx({ json, onEvent })
|
|
83302
83528
|
);
|
|
83303
|
-
if (status) status.done(true, `Found ${result.found ?? 0}, saved ${result.saved ?? 0} new`);
|
|
83304
83529
|
autoSync();
|
|
83305
|
-
if (json)
|
|
83306
|
-
|
|
83530
|
+
if (json) {
|
|
83531
|
+
emitJson(result);
|
|
83532
|
+
} else {
|
|
83533
|
+
say();
|
|
83534
|
+
say(c.green(` Saved ${result.saved ?? streamed} new lead${(result.saved ?? streamed) === 1 ? "" : "s"}.`));
|
|
83535
|
+
if (result.note) say(c.dim(" " + result.note));
|
|
83536
|
+
say();
|
|
83537
|
+
}
|
|
83307
83538
|
} catch (e) {
|
|
83308
|
-
if (status) status.done(false, "Find failed");
|
|
83309
83539
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
83310
83540
|
process.exitCode = 1;
|
|
83311
83541
|
}
|
|
@@ -83403,6 +83633,48 @@ async function cmdOutreach(argv) {
|
|
|
83403
83633
|
if (result.noPhone) say(c.dim(` ${result.noPhone} had no phone number and were skipped for messaging.`));
|
|
83404
83634
|
say();
|
|
83405
83635
|
}
|
|
83636
|
+
async function cmdBook(argv) {
|
|
83637
|
+
const { flags, rest } = parseArgs(argv);
|
|
83638
|
+
const json = !!flags.json;
|
|
83639
|
+
const id = Number(flags.id ?? rest[0]);
|
|
83640
|
+
if (!id || Number.isNaN(id)) {
|
|
83641
|
+
const err = 'Usage: sproutkit book <leadId> [--standalone] [--instructions "..."] [--email owner@x.com]';
|
|
83642
|
+
json ? emitJson({ ok: false, error: err }) : say(c.red(" " + err));
|
|
83643
|
+
process.exitCode = 1;
|
|
83644
|
+
return;
|
|
83645
|
+
}
|
|
83646
|
+
const standalone = !!flags.standalone;
|
|
83647
|
+
const status = json ? void 0 : new Status();
|
|
83648
|
+
if (status) {
|
|
83649
|
+
status.start(standalone ? `Building standalone booking site for lead #${id}` : `Adding booking to lead #${id}`);
|
|
83650
|
+
}
|
|
83651
|
+
try {
|
|
83652
|
+
const result = await runTool(
|
|
83653
|
+
"add_booking_site",
|
|
83654
|
+
{
|
|
83655
|
+
id,
|
|
83656
|
+
standalone,
|
|
83657
|
+
instructions: flags.instructions ? String(flags.instructions) : void 0,
|
|
83658
|
+
archetype: flags.archetype ? String(flags.archetype) : void 0,
|
|
83659
|
+
ownerEmail: flags.email ? String(flags.email) : void 0
|
|
83660
|
+
},
|
|
83661
|
+
makeCtx({ json, status })
|
|
83662
|
+
);
|
|
83663
|
+
if (result && result.ok === false) {
|
|
83664
|
+
if (status) status.done(false, result.error ?? "Booking failed");
|
|
83665
|
+
if (json) emitJson(result);
|
|
83666
|
+
process.exitCode = 1;
|
|
83667
|
+
return;
|
|
83668
|
+
}
|
|
83669
|
+
if (status) status.done(true, result.bookingUrl ? `Booking live at ${result.bookingUrl}` : `Booking added to lead #${id}`);
|
|
83670
|
+
autoSync();
|
|
83671
|
+
if (json) emitJson(result);
|
|
83672
|
+
} catch (e) {
|
|
83673
|
+
if (status) status.done(false, "Booking failed");
|
|
83674
|
+
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
83675
|
+
process.exitCode = 1;
|
|
83676
|
+
}
|
|
83677
|
+
}
|
|
83406
83678
|
async function cmdBuildAll(argv) {
|
|
83407
83679
|
const { flags } = parseArgs(argv);
|
|
83408
83680
|
const json = !!flags.json;
|
|
@@ -83502,7 +83774,7 @@ async function main() {
|
|
|
83502
83774
|
closeTty();
|
|
83503
83775
|
return;
|
|
83504
83776
|
}
|
|
83505
|
-
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "outreach") {
|
|
83777
|
+
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "book" || arg === "outreach") {
|
|
83506
83778
|
const subArgs = process.argv.slice(3);
|
|
83507
83779
|
const lic2 = await check();
|
|
83508
83780
|
if (lic2.status !== "active") {
|
|
@@ -83517,6 +83789,7 @@ async function main() {
|
|
|
83517
83789
|
else if (arg === "list") await cmdList(subArgs);
|
|
83518
83790
|
else if (arg === "build") await cmdBuild(subArgs);
|
|
83519
83791
|
else if (arg === "build-all") await cmdBuildAll(subArgs);
|
|
83792
|
+
else if (arg === "book") await cmdBook(subArgs);
|
|
83520
83793
|
else await cmdOutreach(subArgs);
|
|
83521
83794
|
closeTty();
|
|
83522
83795
|
return;
|
|
@@ -83531,6 +83804,7 @@ async function main() {
|
|
|
83531
83804
|
say(` ${c.bold("sproutkit list")} list leads (--status, --city, --json)`);
|
|
83532
83805
|
say(` ${c.bold("sproutkit build <id>")} build a website (--instructions, --cms)`);
|
|
83533
83806
|
say(` ${c.bold("sproutkit build-all")} build many at once (--ids, --concurrency 3)`);
|
|
83807
|
+
say(` ${c.bold("sproutkit book <id>")} booking site (--standalone for own-URL)`);
|
|
83534
83808
|
say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
|
|
83535
83809
|
say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
|
|
83536
83810
|
say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -48,6 +48,17 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
48
48
|
renders the progress. One `sproutkit build-all` call is correct; N parallel
|
|
49
49
|
`sproutkit build` calls via subagents is not.
|
|
50
50
|
|
|
51
|
+
- **Add a booking site** — `sproutkit book <leadId> --json`
|
|
52
|
+
- Default: adds an online booking/reservation form onto a site Sproutkit
|
|
53
|
+
already built for this lead (same domain, at /book).
|
|
54
|
+
- `--standalone`: builds a self-contained booking site with its OWN url, for
|
|
55
|
+
a client who already has their own website and just needs a booking page to
|
|
56
|
+
link to. No Sproutkit-built site required.
|
|
57
|
+
- Optional: `--instructions "45-min slots, add beard trim £12"`,
|
|
58
|
+
`--archetype slots|reservation|enquiry`, `--email owner@x.com`.
|
|
59
|
+
- Use `book --standalone` when the user says the client "already has a
|
|
60
|
+
website" or "just needs a booking page".
|
|
61
|
+
|
|
51
62
|
- **Prepare outreach** — `sproutkit outreach --json` (optional `--ids 3,5,9`)
|
|
52
63
|
- Fills the message template, moves the leads to Contacted, and returns a
|
|
53
64
|
link per lead to open in WhatsApp/SMS. It does NOT auto-send — give the user
|