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 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: "Add an online booking / reservation form to a lead's EXISTING website (the site must already be built). Sproutkit designs a booking form suited to the business \u2014 appointment slots for barbers/salons, table reservations for restaurants, or an enquiry form \u2014 and deploys it onto the same domain. Use this for requests like 'add a booking system to The Porch' or 'give the barber online appointments'. Pass `instructions` to customise it (services, slot length, party size, occasion, copy) and `archetype` to force a specific style. Costs a Claude call + a Vercel redeploy.",
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. Its website must already be built." },
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.searchAndSaveLeads)({
81110
- placesKey,
81111
- // searchPlaces uses `city` as the text query, so keep the user's words.
81112
- city: location,
81113
- types: categories,
81114
- maxResults: Math.min(Number(input.maxResults ?? 25), 100),
81115
- center: place ? { lat: place.lat, lng: place.lng } : void 0,
81116
- radiusMeters: Number(input.radiusMeters ?? 3e3)
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 yet. Build the site first, then add booking \u2014 bookings live on the same domain."
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.5.1",
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
- const status = json ? void 0 : new Status();
83291
- if (status) status.start(`Finding leads in ${location}`);
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, status })
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) emitJson(result);
83306
- else if (result.note) say(c.dim(" " + result.note));
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`);
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.5.1",
3
+ "version": "0.7.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"