sproutkit-code 0.19.0 → 1.1.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
@@ -69072,9 +69072,9 @@ var require_mock_utils = __commonJS({
69072
69072
  if (mockDispatch2.data.callback) {
69073
69073
  mockDispatch2.data = { ...mockDispatch2.data, ...mockDispatch2.data.callback(opts) };
69074
69074
  }
69075
- const { data: { statusCode, data, headers, trailers, error }, delay, persist: persist2 } = mockDispatch2;
69075
+ const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch2;
69076
69076
  const { timesInvoked, times } = mockDispatch2;
69077
- mockDispatch2.consumed = !persist2 && timesInvoked >= times;
69077
+ mockDispatch2.consumed = !persist && timesInvoked >= times;
69078
69078
  mockDispatch2.pending = timesInvoked < times;
69079
69079
  if (error !== null) {
69080
69080
  deleteMockDispatch(this[kDispatches], key);
@@ -69494,14 +69494,14 @@ var require_pending_interceptors_formatter = __commonJS({
69494
69494
  }
69495
69495
  format(pendingInterceptors) {
69496
69496
  const withPrettyHeaders = pendingInterceptors.map(
69497
- ({ method, path: path3, data: { statusCode }, persist: persist2, times, timesInvoked, origin }) => ({
69497
+ ({ method, path: path3, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
69498
69498
  Method: method,
69499
69499
  Origin: origin,
69500
69500
  Path: path3,
69501
69501
  "Status code": statusCode,
69502
- Persistent: persist2 ? PERSISTENT : NOT_PERSISTENT,
69502
+ Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
69503
69503
  Invocations: timesInvoked,
69504
- Remaining: persist2 ? Infinity : times - timesInvoked
69504
+ Remaining: persist ? Infinity : times - timesInvoked
69505
69505
  })
69506
69506
  );
69507
69507
  this.logger.table(withPrettyHeaders);
@@ -79738,6 +79738,13 @@ var require_builder = __commonJS({
79738
79738
  const pattern = new RegExp(`https?:\\/\\/[^\\s"'\`)]*(${STOCK_HOSTS.map((h) => h.replace(/\./g, "\\.")).join("|")})[^\\s"'\`)]*`, "gi");
79739
79739
  return code.replace(pattern, "");
79740
79740
  }
79741
+ function demoBanner(businessName) {
79742
+ const esc = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
79743
+ const label = `Preview site for ${esc(businessName)}`;
79744
+ 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">
79745
+ <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.
79746
+ </div>`;
79747
+ }
79741
79748
  function snapshotDir(leadId) {
79742
79749
  return path3.join((0, runtime_1.userDataDir)(), "data", "sites", String(leadId));
79743
79750
  }
@@ -80049,28 +80056,84 @@ Whichever you pick, make it FEEL premium and intentional \u2014 not a template.
80049
80056
  - Subtle scroll-revealed details (not heavy animations)
80050
80057
  - One (and only one) accent colour, chosen from the allowed palette above. Use it sparingly \u2014 buttons, key hover states, one accent element per section.
80051
80058
 
80059
+ The direction governs LOOK; the archetype below governs STRUCTURE. Vary both. Also
80060
+ vary the mechanics between sites so they don't feel stamped from one mould \u2014 pick
80061
+ what suits this business rather than defaulting:
80062
+ - Section rhythm: alternating full-bleed bands vs a contained column vs a
80063
+ two-column split that runs down the page.
80064
+ - Content shape: cards vs a plain itemised list vs a table vs numbered steps vs a
80065
+ single long editorial column.
80066
+ - Where the accent lands, whether section backgrounds alternate, whether headings
80067
+ are centred or hard-left, whether the page is wide or narrow.
80068
+
80052
80069
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
80053
- SECTIONS (in this order)
80070
+ STRUCTURE \u2014 design the page around THIS business, do not follow a fixed template
80054
80071
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
80055
- 1. NAV (sticky, blurred bg) \u2014 business name + a "Call us" or "Book now" button linking to tel:${lead.phone}
80056
- 2. HERO \u2014 full viewport. Big headline that captures what this business uniquely does. Subheadline. ONE primary CTA button. If PHOTOS.length > 0 use PHOTOS[0] as the hero image (full-width, large). If PHOTOS.length === 0, use a bold gradient or a rich solid colour background \u2014 NO image.
80057
- 3. ABOUT / WHAT WE DO \u2014 2-3 paragraphs of genuine, on-brand copy. NOT generic. Mention they're in ${lead.city}.
80058
- 4. SERVICES \u2014 3-6 services relevant to "${lead.type}". Each has a title + 1-line description. Use a clean grid or list.
80059
- 5. GALLERY \u2014 ONLY include this section if PHOTOS.length >= 2. Show PHOTOS[1..] in an interesting layout (asymmetric grid, masonry, or large-feature-plus-thumbs). If PHOTOS.length < 2, OMIT the gallery section entirely \u2014 don't replace it with stock imagery or a placeholder.
80060
- 6. REVIEWS \u2014 display the real reviews above as testimonial cards with the customer's name and stars
80061
- 7. CONTACT \u2014 address, phone, hours in a clear card. Map embed NOT required.
80062
- 8. FOOTER \u2014 simple, business name + address + a final "Built with Sproutkit \xB7 sproutkit.app" line in muted text
80072
+ CRITICAL: sites for different kinds of business must not share a skeleton. What a
80073
+ customer needs from a takeaway is not what they need from a plumber. Decide the
80074
+ section set, their ORDER, and how much space each gets based on what actually
80075
+ drives a decision for a "${lead.type}" \u2014 then commit to it.
80076
+
80077
+ Pick the LAYOUT ARCHETYPE that fits this business (or compose your own if none fits):
80078
+
80079
+ A. MENU / PRODUCT-FORWARD \u2014 food & drink (restaurants, cafes, takeaways, bakeries).
80080
+ The menu IS the page. Lead with it high up, itemised with prices, grouped by
80081
+ course/category. Hero can be compact (a band, not full-screen) to get the menu
80082
+ above the fold. Photos of food used large. Hours + "how to order/book" near top.
80083
+
80084
+ B. BOOKING-FORWARD \u2014 appointment trades (barbers, salons, nails, clinics, dentists).
80085
+ The job is "when can I get in": services + prices as a scannable price list,
80086
+ opening hours prominent, a persistent Book/Call action. Staff or before/after
80087
+ imagery if available. Reviews used as reassurance near the booking action.
80088
+
80089
+ C. TRUST & COVERAGE-FORWARD \u2014 home/field trades (plumbers, electricians, builders,
80090
+ roofers, cleaners, car washes, removals). Buyers need confidence + "do you cover
80091
+ me?". Lead with the problem you solve and credentials (years, insured, callout
80092
+ time), then areas covered, then services, then proof (reviews). Phone must be
80093
+ impossible to miss \u2014 a sticky call bar is appropriate. Photos are of work done.
80094
+
80095
+ D. SHOWCASE / PORTFOLIO-FORWARD \u2014 visual businesses (florists, photographers,
80096
+ tattoo, interiors, boutiques). Imagery dominates: full-bleed gallery early and
80097
+ large, minimal copy, the work sells itself. Contact is quiet but easy.
80098
+
80099
+ E. CREDIBILITY-FORWARD \u2014 professional services (accountants, solicitors, agencies,
80100
+ consultants, tutors). Lead with clarity of offer and who it's for, then process
80101
+ or specialisms, then credentials/testimonials. Restrained, text-led, confident.
80102
+
80103
+ RULES for structure:
80104
+ - Use the archetype's priorities to choose and ORDER sections. Genuinely differ:
80105
+ a takeaway should NOT open with a full-screen hero then "About"; a plumber should
80106
+ NOT bury the phone number under a gallery.
80107
+ - OMIT any section you have no real content for. Never render an empty or padded
80108
+ section \u2014 fewer, fuller sections always beat a complete-looking skeleton.
80109
+ - Vary the hero treatment to suit: full-viewport image, compact banner, split
80110
+ text/image, or type-only. Not every site gets the same big centred hero.
80111
+ - Include a NAV appropriate to the site (sticky if the page is long) with the
80112
+ business name and the single most useful action for THIS business type
80113
+ (tel:${lead.phone} for trades, book for appointments, menu/order for food).
80114
+ - Non-negotiable floor, however you arrange it: the business name, what they do,
80115
+ where they are (${lead.city}), the phone as a tel: link, and a footer ending with
80116
+ "Built with Sproutkit \xB7 sproutkit.app" in muted text.
80117
+ - Use the real reviews and hours given above where they earn their place \u2014 not in a
80118
+ fixed slot. Photos: only from PHOTOS, sized per the archetype (see gallery rule:
80119
+ if PHOTOS.length < 2 there is no gallery section at all).
80063
80120
  ${multipage ? `
80064
80121
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
80065
80122
  MULTIPAGE (REQUIRED) \u2014 real separate pages, not one long scroll
80066
80123
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
80067
80124
  Build a MULTI-PAGE site using react-router-dom's HashRouter (import { HashRouter, Routes, Route, Link, useNavigate } from "react-router-dom"). HashRouter is REQUIRED (not BrowserRouter) so routes work on static hosting without server config.
80068
80125
 
80069
- Pages (one <Route> each), with a persistent sticky NAV linking between them via <Link>:
80070
- ${pageList.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}
80071
-
80072
- - Home: hero + a short intro + 2-3 teaser sections that link through to the other pages.
80073
- - Distribute the sections above across the pages sensibly (e.g. Services page = full services; About = the about/story + reviews; Contact = address/phone/hours/CTA). Don't cram everything onto Home.
80126
+ ${pages && pages.length > 0 ? `Pages (one <Route> each), with a persistent NAV linking between them via <Link>:
80127
+ ${pageList.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}` : `Choose the PAGES yourself from what this business actually needs \u2014 do not default
80128
+ to a generic Home/Services/About/Contact set. Base them on the layout archetype
80129
+ below. For example: a takeaway needs Menu (the big one) + Order/Find us; a plumber
80130
+ needs Services + Areas covered + Get a quote; a salon needs Treatments & prices +
80131
+ Book + Gallery; a florist needs Gallery/Shop + Occasions + Contact. 3-5 pages,
80132
+ each with a real reason to exist. Put them in one <Route> each with a persistent
80133
+ NAV linking between them via <Link>.`}
80134
+
80135
+ - Home: a focused landing \u2014 the single most compelling thing about this business plus the primary action, then short teasers that link onward. Not a dump of every section.
80136
+ - Give each page enough real content to justify itself; if a page would be thin, merge it rather than padding it.
80074
80137
  - The whole thing is STILL one App.jsx exporting a single default App component that renders <HashRouter>\u2026</HashRouter>. Define each page as a component in the same file.
80075
80138
  - Nav must be present on every page and highlight the active route. Include the phone/Book CTA in the nav.
80076
80139
  ` : ""}
@@ -80095,6 +80158,14 @@ CMS INTEGRATION (MANDATORY when this brief mentions it)
80095
80158
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
80096
80159
  This site is editable by the business owner via a CMS. You MUST wire it up:
80097
80160
 
80161
+ IMPORTANT \u2014 this schema is a CONTENT contract, not a LAYOUT one. The keys below
80162
+ exist so the /admin editor knows what it can edit; they do NOT dictate the page's
80163
+ structure or order. Still follow the layout archetype above: render these fields
80164
+ wherever that structure puts them, give them the weight that business needs (e.g.
80165
+ services as a priced menu high on the page for a takeaway), and omit anything you
80166
+ have no content for. Do not let this schema flatten every CMS site into the same
80167
+ hero \u2192 about \u2192 services \u2192 contact page.
80168
+
80098
80169
  1. At the top of App.jsx, declare a FALLBACK object with all the copy you'd
80099
80170
  normally hardcode:
80100
80171
  const FALLBACK = {
@@ -80247,7 +80318,7 @@ ${prompt}` : prompt;
80247
80318
  onProgress("Deploying to Vercel...");
80248
80319
  const indexHtml = `<!DOCTYPE html>
80249
80320
  <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head>
80250
- <body style="margin:0"><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
80321
+ <body style="margin:0">${demoBanner(lead.name)}<div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
80251
80322
  const mainJsx = `import { StrictMode } from 'react';
80252
80323
  import { createRoot } from 'react-dom/client';
80253
80324
  import App from './App.jsx';
@@ -80442,7 +80513,7 @@ export default defineConfig({ plugins: [react()] });`;
80442
80513
  { source: "/book/", destination: "/book/index.html" }
80443
80514
  ]
80444
80515
  });
80445
- 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"><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
80516
+ 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>`;
80446
80517
  const mainJsxCms = `import { StrictMode } from 'react';
80447
80518
  import { createRoot } from 'react-dom/client';
80448
80519
  import App from './App.jsx';
@@ -80519,7 +80590,7 @@ export default defineConfig({ plugins: [react()] });`;
80519
80590
  snapshot[appJsxIndex] = { ...snapshot[appJsxIndex], data: modifiedApp };
80520
80591
  saveSnapshot(lead.id, snapshot);
80521
80592
  const files = snapshot.map((f) => ({ file: f.file, data: f.data, encoding: f.encoding }));
80522
- 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"><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>`;
80593
+ 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>`;
80523
80594
  const mainJsx = `import { StrictMode } from 'react';
80524
80595
  import { createRoot } from 'react-dom/client';
80525
80596
  import App from './App.jsx';
@@ -93066,6 +93137,11 @@ var require_chat_tools = __commonJS({
93066
93137
  type: "array",
93067
93138
  items: { type: "number" },
93068
93139
  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')."
93140
+ },
93141
+ channel: {
93142
+ type: "string",
93143
+ enum: ["whatsapp", "sms"],
93144
+ 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."
93069
93145
  }
93070
93146
  }
93071
93147
  },
@@ -93417,7 +93493,8 @@ Interested?`;
93417
93493
  };
93418
93494
  }
93419
93495
  const channelRow = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_channel"]);
93420
- const channel = channelRow?.value || "whatsapp";
93496
+ const requested = String(input.channel ?? "").toLowerCase();
93497
+ const channel = requested === "sms" || requested === "whatsapp" ? requested : channelRow?.value || "whatsapp";
93421
93498
  const template = outreachTemplate(db2);
93422
93499
  const smtp = (0, email_1.smtpFromSettings)((k) => getDecryptedSetting(k));
93423
93500
  const emailSubject = getDecryptedSetting("outreach_email_subject") || "A free demo website for {name}";
@@ -94597,7 +94674,7 @@ var require_package2 = __commonJS({
94597
94674
  "package.json"(exports2, module2) {
94598
94675
  module2.exports = {
94599
94676
  name: "sproutkit-code",
94600
- version: "0.19.0",
94677
+ version: "1.1.0",
94601
94678
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
94602
94679
  bin: {
94603
94680
  sproutkit: "./dist/index.js"
@@ -95159,6 +95236,235 @@ async function runOnboarding(force = false) {
95159
95236
  return true;
95160
95237
  }
95161
95238
 
95239
+ // src/update.ts
95240
+ var import_child_process2 = require("child_process");
95241
+ var import_util = require("util");
95242
+ var execFileP = (0, import_util.promisify)(import_child_process2.execFile);
95243
+ var PKG = "sproutkit-code";
95244
+ var CURRENT_VERSION = require_package2().version;
95245
+ async function latestOnRegistry() {
95246
+ try {
95247
+ const r = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
95248
+ signal: AbortSignal.timeout(5e3)
95249
+ });
95250
+ if (!r.ok) return null;
95251
+ const j = await r.json();
95252
+ return j.version ?? null;
95253
+ } catch {
95254
+ return null;
95255
+ }
95256
+ }
95257
+ function isNewer(a, b) {
95258
+ const pa = a.split(".").map(Number);
95259
+ const pb = b.split(".").map(Number);
95260
+ for (let i = 0; i < 3; i++) {
95261
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
95262
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
95263
+ }
95264
+ return false;
95265
+ }
95266
+ async function runUpdate() {
95267
+ say(c.dim(` Current version ${CURRENT_VERSION}. Checking npm\u2026`));
95268
+ const latest = await latestOnRegistry();
95269
+ if (!latest) {
95270
+ say(c.amber(" Couldn't reach npm. Check your connection and try again."));
95271
+ return;
95272
+ }
95273
+ if (!isNewer(latest, CURRENT_VERSION)) {
95274
+ say(c.green(` \u2713 You're on the latest version (${CURRENT_VERSION}).`));
95275
+ return;
95276
+ }
95277
+ say(c.dim(` Updating ${CURRENT_VERSION} \u2192 ${latest}\u2026`));
95278
+ try {
95279
+ await execFileP("npm", ["install", "-g", `${PKG}@latest`], { timeout: 12e4 });
95280
+ say(c.green(` \u2713 Updated to ${latest}. Restart sproutkit to use it.`));
95281
+ } catch (e) {
95282
+ say(c.red(` Update failed: ${e.message}`));
95283
+ say(c.dim(` Try manually: npm install -g ${PKG}@latest`));
95284
+ }
95285
+ }
95286
+ async function notifyIfOutdated() {
95287
+ try {
95288
+ const last = Number(getSetting("update_checked_at") ?? 0);
95289
+ if (Date.now() - last < 24 * 60 * 60 * 1e3) return;
95290
+ setSetting("update_checked_at", String(Date.now()));
95291
+ const latest = await latestOnRegistry();
95292
+ if (latest && isNewer(latest, CURRENT_VERSION)) {
95293
+ say(
95294
+ c.amber(
95295
+ ` \u2191 Update available: ${CURRENT_VERSION} \u2192 ${latest}. Run 'sproutkit update'.`
95296
+ )
95297
+ );
95298
+ say();
95299
+ }
95300
+ } catch {
95301
+ }
95302
+ }
95303
+
95304
+ // src/sync.ts
95305
+ var import_os3 = __toESM(require("os"));
95306
+ var import_path2 = __toESM(require("path"));
95307
+ var import_fs2 = __toESM(require("fs"));
95308
+ var COLS = [
95309
+ "place_id",
95310
+ "name",
95311
+ "type",
95312
+ "city",
95313
+ "address",
95314
+ "phone",
95315
+ "website",
95316
+ "rating",
95317
+ "review_count",
95318
+ "lat",
95319
+ "lng",
95320
+ "status",
95321
+ "site_url",
95322
+ "site_built_at",
95323
+ "contacted_at",
95324
+ "notes",
95325
+ "score",
95326
+ "price_level",
95327
+ "business_status"
95328
+ ];
95329
+ function desktopDbPath() {
95330
+ const home = import_os3.default.homedir();
95331
+ const candidates = [];
95332
+ if (process.platform === "darwin") {
95333
+ candidates.push(import_path2.default.join(home, "Library", "Application Support", "Sproutkit", "data", "sproutio.db"));
95334
+ } else if (process.platform === "win32") {
95335
+ const appData = process.env.APPDATA || import_path2.default.join(home, "AppData", "Roaming");
95336
+ candidates.push(import_path2.default.join(appData, "Sproutkit", "data", "sproutio.db"));
95337
+ } else {
95338
+ const cfg = process.env.XDG_CONFIG_HOME || import_path2.default.join(home, ".config");
95339
+ candidates.push(import_path2.default.join(cfg, "Sproutkit", "data", "sproutio.db"));
95340
+ }
95341
+ return candidates.find((p) => import_fs2.default.existsSync(p)) ?? null;
95342
+ }
95343
+ function syncToDesktop(opts = {}) {
95344
+ const dest = desktopDbPath();
95345
+ if (!dest) return { ok: false, reason: "no_desktop_db", inserted: 0, updated: 0 };
95346
+ const cliLeads = getDb().all(
95347
+ `SELECT ${COLS.join(", ")} FROM leads WHERE place_id IS NOT NULL AND place_id != ''`,
95348
+ []
95349
+ );
95350
+ if (cliLeads.length === 0) return { ok: true, inserted: 0, updated: 0 };
95351
+ const { Database } = require("node-sqlite3-wasm");
95352
+ const ddb = new Database(dest);
95353
+ let inserted = 0;
95354
+ let updated = 0;
95355
+ try {
95356
+ for (const lead of cliLeads) {
95357
+ const existing = ddb.get("SELECT id FROM leads WHERE place_id = ?", [lead.place_id]);
95358
+ if (existing) {
95359
+ ddb.run(
95360
+ "UPDATE leads SET status = ?, site_url = ?, site_built_at = ?, score = ? WHERE place_id = ?",
95361
+ [lead.status, lead.site_url, lead.site_built_at, lead.score, lead.place_id].map(
95362
+ (v) => v ?? null
95363
+ )
95364
+ );
95365
+ updated++;
95366
+ } else {
95367
+ const placeholders = COLS.map(() => "?").join(", ");
95368
+ ddb.run(
95369
+ `INSERT INTO leads (${COLS.join(", ")}) VALUES (${placeholders})`,
95370
+ COLS.map((k) => lead[k] ?? null)
95371
+ );
95372
+ inserted++;
95373
+ }
95374
+ }
95375
+ } finally {
95376
+ ddb.close();
95377
+ }
95378
+ if (!opts.quiet) {
95379
+ say(
95380
+ c.green(` \u2713 Synced to desktop: ${inserted} new, ${updated} updated.`)
95381
+ );
95382
+ }
95383
+ return { ok: true, inserted, updated };
95384
+ }
95385
+ function autoSync() {
95386
+ try {
95387
+ const r = syncToDesktop({ quiet: true });
95388
+ if (r.ok && (r.inserted || r.updated)) {
95389
+ say(c.dim(` \u2194 synced to desktop (${r.inserted} new, ${r.updated} updated)`));
95390
+ }
95391
+ } catch {
95392
+ }
95393
+ }
95394
+
95395
+ // src/models-cmd.ts
95396
+ async function runModelsClaudeCode() {
95397
+ const current = getClaudeCodeModel();
95398
+ say();
95399
+ say(c.bold(" Model (Claude subscription):"));
95400
+ say(c.dim(" Uses your Claude Code login \u2014 pick which model builds run on."));
95401
+ CLAUDE_CODE_MODELS.forEach((m, i) => {
95402
+ const active = m.alias === current ? c.green(" \u2190 current") : "";
95403
+ const isDefault = !current && m.alias === "sonnet" ? c.dim(" (CLI default)") : "";
95404
+ say(` ${c.greenBold(String(i + 1).padStart(2))} ${m.label} ${c.dim(m.alias)}${active}${isDefault}`);
95405
+ });
95406
+ say();
95407
+ const pick = await ask(c.green(" Pick a number to switch (or Enter to keep current): "));
95408
+ if (!pick) {
95409
+ say(c.dim(" No change."));
95410
+ return;
95411
+ }
95412
+ const idx = Number(pick) - 1;
95413
+ if (Number.isNaN(idx) || idx < 0 || idx >= CLAUDE_CODE_MODELS.length) {
95414
+ say(c.amber(" Not a valid choice \u2014 no change made."));
95415
+ return;
95416
+ }
95417
+ const chosen = CLAUDE_CODE_MODELS[idx];
95418
+ setSetting("claude_code_model", chosen.alias);
95419
+ say(c.green(` \u2713 Builds will now use ${chosen.alias}.`));
95420
+ if (chosen.alias === "fable") {
95421
+ say(c.dim(" Note: Fable needs 30-day data retention on your Claude account, or it'll error."));
95422
+ }
95423
+ }
95424
+ async function runModels() {
95425
+ if (getBackend() === "claude_code") {
95426
+ await runModelsClaudeCode();
95427
+ return;
95428
+ }
95429
+ const apiKey = getSetting("anthropic_key");
95430
+ if (!apiKey) {
95431
+ say(c.red(" No Anthropic key set. Run 'sproutkit setup' first."));
95432
+ return;
95433
+ }
95434
+ say(c.dim(" Fetching your available models from Anthropic\u2026"));
95435
+ let models2 = listCachedModels();
95436
+ try {
95437
+ models2 = await refreshModels(apiKey);
95438
+ } catch (e) {
95439
+ say(c.amber(` Couldn't refresh from the API (${e.message}).`));
95440
+ say(c.dim(" Showing the last known list instead."));
95441
+ }
95442
+ if (models2.length === 0) {
95443
+ say(c.red(" No models returned. Check your Anthropic key and connection."));
95444
+ return;
95445
+ }
95446
+ const current = getPreferredModel();
95447
+ say();
95448
+ say(c.bold(" Available models:"));
95449
+ models2.forEach((m, i) => {
95450
+ const active = m.id === current ? c.green(" \u2190 current") : "";
95451
+ say(` ${c.greenBold(String(i + 1).padStart(2))} ${m.display_name ?? m.id} ${c.dim(m.id)}${active}`);
95452
+ });
95453
+ say();
95454
+ const pick = await ask(c.green(" Pick a number to switch (or Enter to keep current): "));
95455
+ if (!pick) {
95456
+ say(c.dim(" Kept current model."));
95457
+ return;
95458
+ }
95459
+ const idx = Number(pick) - 1;
95460
+ if (Number.isNaN(idx) || idx < 0 || idx >= models2.length) {
95461
+ say(c.amber(" Not a valid choice \u2014 no change made."));
95462
+ return;
95463
+ }
95464
+ setSetting("claude_model", models2[idx].id);
95465
+ say(c.green(` \u2713 Now using ${models2[idx].display_name ?? models2[idx].id}.`));
95466
+ }
95467
+
95162
95468
  // src/invoke-ipc.ts
95163
95469
  async function requireBlobToken(vercelToken, emit, leadId) {
95164
95470
  const existing = getSetting("blob_token");
@@ -95308,357 +95614,6 @@ function makeInvokeIpc(emit) {
95308
95614
  };
95309
95615
  }
95310
95616
 
95311
- // src/sync.ts
95312
- var import_os3 = __toESM(require("os"));
95313
- var import_path2 = __toESM(require("path"));
95314
- var import_fs2 = __toESM(require("fs"));
95315
- var COLS = [
95316
- "place_id",
95317
- "name",
95318
- "type",
95319
- "city",
95320
- "address",
95321
- "phone",
95322
- "website",
95323
- "rating",
95324
- "review_count",
95325
- "lat",
95326
- "lng",
95327
- "status",
95328
- "site_url",
95329
- "site_built_at",
95330
- "contacted_at",
95331
- "notes",
95332
- "score",
95333
- "price_level",
95334
- "business_status"
95335
- ];
95336
- function desktopDbPath() {
95337
- const home = import_os3.default.homedir();
95338
- const candidates = [];
95339
- if (process.platform === "darwin") {
95340
- candidates.push(import_path2.default.join(home, "Library", "Application Support", "Sproutkit", "data", "sproutio.db"));
95341
- } else if (process.platform === "win32") {
95342
- const appData = process.env.APPDATA || import_path2.default.join(home, "AppData", "Roaming");
95343
- candidates.push(import_path2.default.join(appData, "Sproutkit", "data", "sproutio.db"));
95344
- } else {
95345
- const cfg = process.env.XDG_CONFIG_HOME || import_path2.default.join(home, ".config");
95346
- candidates.push(import_path2.default.join(cfg, "Sproutkit", "data", "sproutio.db"));
95347
- }
95348
- return candidates.find((p) => import_fs2.default.existsSync(p)) ?? null;
95349
- }
95350
- function syncToDesktop(opts = {}) {
95351
- const dest = desktopDbPath();
95352
- if (!dest) return { ok: false, reason: "no_desktop_db", inserted: 0, updated: 0 };
95353
- const cliLeads = getDb().all(
95354
- `SELECT ${COLS.join(", ")} FROM leads WHERE place_id IS NOT NULL AND place_id != ''`,
95355
- []
95356
- );
95357
- if (cliLeads.length === 0) return { ok: true, inserted: 0, updated: 0 };
95358
- const { Database } = require("node-sqlite3-wasm");
95359
- const ddb = new Database(dest);
95360
- let inserted = 0;
95361
- let updated = 0;
95362
- try {
95363
- for (const lead of cliLeads) {
95364
- const existing = ddb.get("SELECT id FROM leads WHERE place_id = ?", [lead.place_id]);
95365
- if (existing) {
95366
- ddb.run(
95367
- "UPDATE leads SET status = ?, site_url = ?, site_built_at = ?, score = ? WHERE place_id = ?",
95368
- [lead.status, lead.site_url, lead.site_built_at, lead.score, lead.place_id].map(
95369
- (v) => v ?? null
95370
- )
95371
- );
95372
- updated++;
95373
- } else {
95374
- const placeholders = COLS.map(() => "?").join(", ");
95375
- ddb.run(
95376
- `INSERT INTO leads (${COLS.join(", ")}) VALUES (${placeholders})`,
95377
- COLS.map((k) => lead[k] ?? null)
95378
- );
95379
- inserted++;
95380
- }
95381
- }
95382
- } finally {
95383
- ddb.close();
95384
- }
95385
- if (!opts.quiet) {
95386
- say(
95387
- c.green(` \u2713 Synced to desktop: ${inserted} new, ${updated} updated.`)
95388
- );
95389
- }
95390
- return { ok: true, inserted, updated };
95391
- }
95392
- function autoSync() {
95393
- try {
95394
- const r = syncToDesktop({ quiet: true });
95395
- if (r.ok && (r.inserted || r.updated)) {
95396
- say(c.dim(` \u2194 synced to desktop (${r.inserted} new, ${r.updated} updated)`));
95397
- }
95398
- } catch {
95399
- }
95400
- }
95401
-
95402
- // src/chat-repl.ts
95403
- function loadHistory() {
95404
- const rows = getDb().all(
95405
- "SELECT role, content FROM chat_messages WHERE role IN ('user','assistant') ORDER BY id DESC LIMIT 20",
95406
- []
95407
- );
95408
- return rows.reverse();
95409
- }
95410
- function persist(role, content, toolCalls) {
95411
- getDb().run("INSERT INTO chat_messages (role, content, tool_calls) VALUES (?, ?, ?)", [
95412
- role,
95413
- content,
95414
- toolCalls ? JSON.stringify(toolCalls) : null
95415
- ]);
95416
- }
95417
- function labelFor(t) {
95418
- const i = t.input ?? {};
95419
- switch (t.name) {
95420
- case "find_leads":
95421
- return `Find ${i.categories?.join(", ") ?? "businesses"}${i.location ? ` in ${i.location}` : ""}`;
95422
- case "build_site":
95423
- return `Build website for lead #${i.id}`;
95424
- case "build_all_sites":
95425
- return i.leadIds?.length ? `Build ${i.leadIds.length} websites` : "Build all unbuilt sites";
95426
- case "regenerate_site":
95427
- return `Rebuild website for lead #${i.id}`;
95428
- case "add_booking_site":
95429
- return `Add booking site to lead #${i.id}`;
95430
- case "provision_cms":
95431
- return `Set up CMS for lead #${i.id}`;
95432
- case "prepare_outreach":
95433
- return "Prepare outreach";
95434
- case "delete_lead":
95435
- return `Delete lead #${i.id}`;
95436
- default:
95437
- return t.name;
95438
- }
95439
- }
95440
- async function runTurn(userMessage) {
95441
- const apiKey = getSetting("anthropic_key");
95442
- if (!apiKey) {
95443
- if (getBackend() === "claude_code") {
95444
- say(c.amber(" You're on the Claude subscription backend, which powers site building."));
95445
- say(c.dim(" The chat assistant (lead-finding & outreach dispatch) additionally needs an"));
95446
- say(c.dim(" Anthropic API key. Add one with 'sproutkit setup', or build sites directly:"));
95447
- say(c.dim(' sproutkit "build a site for <business>"'));
95448
- } else {
95449
- say(c.red(" No Anthropic key set. Run 'sproutkit setup'."));
95450
- }
95451
- return;
95452
- }
95453
- persist("user", userMessage);
95454
- const history = loadHistory().slice(0, -1);
95455
- const confirmations = {};
95456
- const completedResults = {};
95457
- const status = new Status();
95458
- let statusActive = false;
95459
- let lastProgress = "";
95460
- const emit = (_channel, payload) => {
95461
- const p = payload;
95462
- if (p?.msg && p.msg !== lastProgress) {
95463
- lastProgress = p.msg;
95464
- if (statusActive) status.update(p.msg);
95465
- }
95466
- };
95467
- const invokeIpc = makeInvokeIpc(emit);
95468
- while (true) {
95469
- const seenRunning = /* @__PURE__ */ new Set();
95470
- const result = await chatWithTools({
95471
- apiKey,
95472
- ctx: {
95473
- invokeIpc,
95474
- history,
95475
- userMessage,
95476
- confirmations,
95477
- completedResults,
95478
- emit: (e) => {
95479
- if (e.type === "tool_call") {
95480
- const t = e.data;
95481
- if (t.status === "pending" && !seenRunning.has(t.id)) {
95482
- seenRunning.add(t.id);
95483
- lastProgress = "";
95484
- status.start(labelFor(t));
95485
- statusActive = true;
95486
- } else if (t.status === "completed") {
95487
- status.done(true, labelFor(t));
95488
- statusActive = false;
95489
- } else if (t.status === "error") {
95490
- status.done(false, `${labelFor(t)} \u2014 ${t.error ?? "failed"}`);
95491
- statusActive = false;
95492
- }
95493
- }
95494
- },
95495
- emitIpc: emit
95496
- }
95497
- });
95498
- const pending = result.toolCalls.filter((t) => t.status === "awaiting_confirmation");
95499
- if (pending.length === 0) {
95500
- if (result.text) {
95501
- say();
95502
- say(` ${result.text.replace(/\n/g, "\n ")}`);
95503
- say();
95504
- persist("assistant", result.text, result.toolCalls);
95505
- }
95506
- const ranSomething = result.toolCalls.some((t) => t.status === "completed");
95507
- if (ranSomething) autoSync();
95508
- return;
95509
- }
95510
- if (statusActive) {
95511
- status.done(false, "paused for confirmation");
95512
- statusActive = false;
95513
- }
95514
- say();
95515
- say(c.amber(` ${pending.length} action${pending.length === 1 ? "" : "s"} need confirmation:`));
95516
- for (const t of pending) {
95517
- const ans = (await ask(c.amber(` ${labelFor(t)} \u2014 run it? [y/N] `))).toLowerCase();
95518
- confirmations[t.confirmKey ?? t.id] = ans === "y" || ans === "yes" ? "confirmed" : "rejected";
95519
- }
95520
- say();
95521
- }
95522
- }
95523
-
95524
- // src/update.ts
95525
- var import_child_process2 = require("child_process");
95526
- var import_util = require("util");
95527
- var execFileP = (0, import_util.promisify)(import_child_process2.execFile);
95528
- var PKG = "sproutkit-code";
95529
- var CURRENT_VERSION = require_package2().version;
95530
- async function latestOnRegistry() {
95531
- try {
95532
- const r = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
95533
- signal: AbortSignal.timeout(5e3)
95534
- });
95535
- if (!r.ok) return null;
95536
- const j = await r.json();
95537
- return j.version ?? null;
95538
- } catch {
95539
- return null;
95540
- }
95541
- }
95542
- function isNewer(a, b) {
95543
- const pa = a.split(".").map(Number);
95544
- const pb = b.split(".").map(Number);
95545
- for (let i = 0; i < 3; i++) {
95546
- if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
95547
- if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
95548
- }
95549
- return false;
95550
- }
95551
- async function runUpdate() {
95552
- say(c.dim(` Current version ${CURRENT_VERSION}. Checking npm\u2026`));
95553
- const latest = await latestOnRegistry();
95554
- if (!latest) {
95555
- say(c.amber(" Couldn't reach npm. Check your connection and try again."));
95556
- return;
95557
- }
95558
- if (!isNewer(latest, CURRENT_VERSION)) {
95559
- say(c.green(` \u2713 You're on the latest version (${CURRENT_VERSION}).`));
95560
- return;
95561
- }
95562
- say(c.dim(` Updating ${CURRENT_VERSION} \u2192 ${latest}\u2026`));
95563
- try {
95564
- await execFileP("npm", ["install", "-g", `${PKG}@latest`], { timeout: 12e4 });
95565
- say(c.green(` \u2713 Updated to ${latest}. Restart sproutkit to use it.`));
95566
- } catch (e) {
95567
- say(c.red(` Update failed: ${e.message}`));
95568
- say(c.dim(` Try manually: npm install -g ${PKG}@latest`));
95569
- }
95570
- }
95571
- async function notifyIfOutdated() {
95572
- try {
95573
- const last = Number(getSetting("update_checked_at") ?? 0);
95574
- if (Date.now() - last < 24 * 60 * 60 * 1e3) return;
95575
- setSetting("update_checked_at", String(Date.now()));
95576
- const latest = await latestOnRegistry();
95577
- if (latest && isNewer(latest, CURRENT_VERSION)) {
95578
- say(
95579
- c.amber(
95580
- ` \u2191 Update available: ${CURRENT_VERSION} \u2192 ${latest}. Run 'sproutkit update'.`
95581
- )
95582
- );
95583
- say();
95584
- }
95585
- } catch {
95586
- }
95587
- }
95588
-
95589
- // src/models-cmd.ts
95590
- async function runModelsClaudeCode() {
95591
- const current = getClaudeCodeModel();
95592
- say();
95593
- say(c.bold(" Model (Claude subscription):"));
95594
- say(c.dim(" Uses your Claude Code login \u2014 pick which model builds run on."));
95595
- CLAUDE_CODE_MODELS.forEach((m, i) => {
95596
- const active = m.alias === current ? c.green(" \u2190 current") : "";
95597
- const isDefault = !current && m.alias === "sonnet" ? c.dim(" (CLI default)") : "";
95598
- say(` ${c.greenBold(String(i + 1).padStart(2))} ${m.label} ${c.dim(m.alias)}${active}${isDefault}`);
95599
- });
95600
- say();
95601
- const pick = await ask(c.green(" Pick a number to switch (or Enter to keep current): "));
95602
- if (!pick) {
95603
- say(c.dim(" No change."));
95604
- return;
95605
- }
95606
- const idx = Number(pick) - 1;
95607
- if (Number.isNaN(idx) || idx < 0 || idx >= CLAUDE_CODE_MODELS.length) {
95608
- say(c.amber(" Not a valid choice \u2014 no change made."));
95609
- return;
95610
- }
95611
- const chosen = CLAUDE_CODE_MODELS[idx];
95612
- setSetting("claude_code_model", chosen.alias);
95613
- say(c.green(` \u2713 Builds will now use ${chosen.alias}.`));
95614
- if (chosen.alias === "fable") {
95615
- say(c.dim(" Note: Fable needs 30-day data retention on your Claude account, or it'll error."));
95616
- }
95617
- }
95618
- async function runModels() {
95619
- if (getBackend() === "claude_code") {
95620
- await runModelsClaudeCode();
95621
- return;
95622
- }
95623
- const apiKey = getSetting("anthropic_key");
95624
- if (!apiKey) {
95625
- say(c.red(" No Anthropic key set. Run 'sproutkit setup' first."));
95626
- return;
95627
- }
95628
- say(c.dim(" Fetching your available models from Anthropic\u2026"));
95629
- let models2 = listCachedModels();
95630
- try {
95631
- models2 = await refreshModels(apiKey);
95632
- } catch (e) {
95633
- say(c.amber(` Couldn't refresh from the API (${e.message}).`));
95634
- say(c.dim(" Showing the last known list instead."));
95635
- }
95636
- if (models2.length === 0) {
95637
- say(c.red(" No models returned. Check your Anthropic key and connection."));
95638
- return;
95639
- }
95640
- const current = getPreferredModel();
95641
- say();
95642
- say(c.bold(" Available models:"));
95643
- models2.forEach((m, i) => {
95644
- const active = m.id === current ? c.green(" \u2190 current") : "";
95645
- say(` ${c.greenBold(String(i + 1).padStart(2))} ${m.display_name ?? m.id} ${c.dim(m.id)}${active}`);
95646
- });
95647
- say();
95648
- const pick = await ask(c.green(" Pick a number to switch (or Enter to keep current): "));
95649
- if (!pick) {
95650
- say(c.dim(" Kept current model."));
95651
- return;
95652
- }
95653
- const idx = Number(pick) - 1;
95654
- if (Number.isNaN(idx) || idx < 0 || idx >= models2.length) {
95655
- say(c.amber(" Not a valid choice \u2014 no change made."));
95656
- return;
95657
- }
95658
- setSetting("claude_model", models2[idx].id);
95659
- say(c.green(` \u2713 Now using ${models2[idx].display_name ?? models2[idx].id}.`));
95660
- }
95661
-
95662
95617
  // src/commands.ts
95663
95618
  function makeCtx(opts) {
95664
95619
  const emit = (channel, payload) => {
@@ -95815,7 +95770,10 @@ async function cmdOutreach(argv) {
95815
95770
  const ids = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
95816
95771
  const result = await runTool(
95817
95772
  "prepare_outreach",
95818
- { ids: ids.length ? ids : void 0 },
95773
+ {
95774
+ ids: ids.length ? ids : void 0,
95775
+ channel: flags.channel ? String(flags.channel).toLowerCase() : void 0
95776
+ },
95819
95777
  makeCtx({ json })
95820
95778
  );
95821
95779
  if (json) {
@@ -96128,8 +96086,7 @@ async function cmdBuildAll(argv) {
96128
96086
  });
96129
96087
  }
96130
96088
 
96131
- // src/claude-bridge.ts
96132
- var import_child_process3 = require("child_process");
96089
+ // src/operating-brief.ts
96133
96090
  var TASK_SYSTEM_PROMPT = `You operate the "sproutkit" CLI on the user's behalf. Sproutkit finds local businesses without websites, builds them demo sites, adds booking, and prepares outreach.
96134
96091
 
96135
96092
  HOW TO ACT \u2014 this is strict:
@@ -96166,99 +96123,9 @@ COMMANDS:
96166
96123
  - sproutkit pipeline x3 --json [--book] [--limit 10] \u2014 full pipeline (build\u2192booking\u2192outreach) for many clients concurrently
96167
96124
 
96168
96125
  If no command fits the request, say so in one line and name the closest thing you can do. Never invent results.`;
96169
- var FREE_SYSTEM_PROMPT = `You are Sproutkit's assistant, talking with the user about their lead-generation and website work. You can run the sproutkit CLI (find, list, build, build-all, book, outreach \u2014 all support --json) via Bash, and you can help with small custom changes and questions. Be conversational and helpful, but stay within Sproutkit's domain (leads, demo sites, booking, outreach). You don't need to collapse everything to a single command here \u2014 it's fine to discuss, then act.`;
96170
- function runClaudeTurn(args) {
96171
- const { message, sessionId, write, freeMode } = args;
96172
- const timeoutMs = args.timeoutMs ?? 6e5;
96173
- const cliArgs = [
96174
- "-p",
96175
- "--output-format",
96176
- "stream-json",
96177
- "--include-partial-messages",
96178
- "--verbose",
96179
- // required for stream_event emission
96180
- // Inject the operating instructions directly so behaviour is precise and
96181
- // doesn't depend on flaky skill discovery. Task mode pins exact commands;
96182
- // free mode allows open conversation.
96183
- "--append-system-prompt",
96184
- freeMode ? FREE_SYSTEM_PROMPT : TASK_SYSTEM_PROMPT,
96185
- // Let Claude drive the sproutkit CLI without hanging on a permission
96186
- // prompt, but ONLY the sproutkit command — nothing else runs unattended.
96187
- "--allowedTools",
96188
- "Bash(sproutkit *)"
96189
- ];
96190
- if (sessionId) cliArgs.push("--resume", sessionId);
96191
- return new Promise((resolve) => {
96192
- let child;
96193
- try {
96194
- child = (0, import_child_process3.spawn)(claudeBin(), cliArgs, { stdio: ["pipe", "pipe", "pipe"] });
96195
- } catch (e) {
96196
- resolve({ sessionId, ok: false, error: e.message });
96197
- return;
96198
- }
96199
- let buf = "";
96200
- let newSessionId = sessionId;
96201
- let sawText = false;
96202
- let settled = false;
96203
- let stderr = "";
96204
- const timer = setTimeout(() => {
96205
- if (settled) return;
96206
- settled = true;
96207
- child.kill("SIGKILL");
96208
- resolve({ sessionId: newSessionId, ok: false, error: `Timed out after ${Math.round(timeoutMs / 1e3)}s.` });
96209
- }, timeoutMs);
96210
- child.stdout.on("data", (d) => {
96211
- buf += d.toString();
96212
- let nl;
96213
- while ((nl = buf.indexOf("\n")) >= 0) {
96214
- const line = buf.slice(0, nl).trim();
96215
- buf = buf.slice(nl + 1);
96216
- if (!line) continue;
96217
- let ev;
96218
- try {
96219
- ev = JSON.parse(line);
96220
- } catch {
96221
- continue;
96222
- }
96223
- if (ev.session_id) newSessionId = ev.session_id;
96224
- if (ev.type === "stream_event" && ev.event?.delta?.type === "text_delta" && typeof ev.event.delta.text === "string") {
96225
- sawText = true;
96226
- write(ev.event.delta.text);
96227
- }
96228
- if (ev.type === "result" && !sawText && typeof ev.result === "string") {
96229
- write(ev.result);
96230
- }
96231
- }
96232
- });
96233
- child.stderr.on("data", (d) => stderr += d.toString());
96234
- child.on("error", (e) => {
96235
- if (settled) return;
96236
- settled = true;
96237
- clearTimeout(timer);
96238
- const msg = e.code === "ENOENT" ? `Claude Code CLI not found. Install it and run 'claude /login'.` : `Failed to run Claude Code: ${e.message}`;
96239
- resolve({ sessionId: newSessionId, ok: false, error: msg });
96240
- });
96241
- child.on("close", (code) => {
96242
- if (settled) return;
96243
- settled = true;
96244
- clearTimeout(timer);
96245
- if (code !== 0 && !sawText) {
96246
- resolve({
96247
- sessionId: newSessionId,
96248
- ok: false,
96249
- error: stderr.trim() || `Claude Code exited with code ${code}.`
96250
- });
96251
- } else {
96252
- resolve({ sessionId: newSessionId, ok: true });
96253
- }
96254
- });
96255
- child.stdin.write(message);
96256
- child.stdin.end();
96257
- });
96258
- }
96259
96126
 
96260
96127
  // src/doctor.ts
96261
- var import_child_process4 = require("child_process");
96128
+ var import_child_process3 = require("child_process");
96262
96129
  function mark(l) {
96263
96130
  return l === "ok" ? c.green("\u2713") : l === "warn" ? c.amber("!") : c.red("\u2717");
96264
96131
  }
@@ -96277,7 +96144,7 @@ async function runDoctor() {
96277
96144
  if (backend === "claude_code") {
96278
96145
  const present = (() => {
96279
96146
  try {
96280
- return (0, import_child_process4.spawnSync)(claudeBin(), ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96147
+ return (0, import_child_process3.spawnSync)(claudeBin(), ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96281
96148
  } catch {
96282
96149
  return false;
96283
96150
  }
@@ -96381,20 +96248,16 @@ async function runDoctor() {
96381
96248
  }
96382
96249
 
96383
96250
  // src/index.ts
96384
- var import_child_process5 = require("child_process");
96251
+ var import_child_process4 = require("child_process");
96385
96252
  process.env.SPROUTKIT_HEADLESS = "1";
96386
96253
  function claudePresent() {
96387
96254
  try {
96388
96255
  const bin = process.env.SPROUTKIT_CLAUDE_BIN || "claude";
96389
- return (0, import_child_process5.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96256
+ return (0, import_child_process4.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96390
96257
  } catch {
96391
96258
  return false;
96392
96259
  }
96393
96260
  }
96394
- function useClaudeBridge() {
96395
- if (getBackend() === "claude_code") return true;
96396
- return claudePresent() && !getSetting("anthropic_key");
96397
- }
96398
96261
  async function main() {
96399
96262
  const arg = process.argv[2];
96400
96263
  initDatabase();
@@ -96434,6 +96297,59 @@ async function main() {
96434
96297
  }
96435
96298
  if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach" || arg === "pipeline") {
96436
96299
  const subArgs = process.argv.slice(3);
96300
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
96301
+ const USAGE = {
96302
+ find: [
96303
+ 'sproutkit find "<area>" --categories "barber shop,cafe"',
96304
+ " --max <n> max leads to save (default 25)",
96305
+ " --radius <m> search radius in metres (default 10000)",
96306
+ " --json machine-readable output"
96307
+ ],
96308
+ list: [
96309
+ "sproutkit list [--status <s>] [--city <c>] [--limit <n>] [--json]",
96310
+ " statuses: discovered, site_built, contacted, replied, won, dead"
96311
+ ],
96312
+ build: [
96313
+ "sproutkit build <leadId> [options]",
96314
+ ' --instructions "\u2026" tailor the design/content',
96315
+ " --cms include the editable CMS",
96316
+ " --multipage real Home/Services/About/Contact pages",
96317
+ " --skip-qa skip the post-build review (faster, lower quality)"
96318
+ ],
96319
+ "build-all": [
96320
+ "sproutkit build-all [--ids 3,5] [--concurrency 3] [--json]",
96321
+ " builds every unbuilt lead unless --ids is given; press q to stop"
96322
+ ],
96323
+ regenerate: [
96324
+ 'sproutkit regenerate <leadId> --instructions "what to change"',
96325
+ " --multipage rebuild as a multipage site"
96326
+ ],
96327
+ cms: ["sproutkit cms <leadId>", " adds the editable CMS to a site we already built"],
96328
+ book: [
96329
+ "sproutkit book <leadId> [options]",
96330
+ " --standalone booking site on its OWN url (client already has a site)",
96331
+ " --archetype <type> slots | reservation | enquiry",
96332
+ ' --instructions "\u2026" e.g. "45-min slots, add beard trim \xA312"',
96333
+ " --email <addr> owner email for booking notifications"
96334
+ ],
96335
+ outreach: [
96336
+ "sproutkit outreach [--ids 3,5] [--channel sms|whatsapp] [--json]",
96337
+ " Per lead: phone -> a messaging link you open; no phone -> emailed (needs SMTP).",
96338
+ " Moves reached leads to Contacted. Targets every lead with a built site",
96339
+ " unless --ids is given."
96340
+ ],
96341
+ pipeline: [
96342
+ "sproutkit pipeline x3 [--book] [--limit 10] [--ids 3,5] [--json]",
96343
+ " build -> (booking) -> outreach for many clients concurrently (max 4)",
96344
+ " press q to stop after in-flight clients finish"
96345
+ ]
96346
+ };
96347
+ say();
96348
+ for (const line of USAGE[arg] ?? [`sproutkit ${arg}`]) say(" " + line);
96349
+ say();
96350
+ closeTty();
96351
+ return;
96352
+ }
96437
96353
  const lic2 = await check();
96438
96354
  if (lic2.status !== "active") {
96439
96355
  const msg = lic2.status === "offline_grace_expired" ? "Licence check offline and grace expired \u2014 reconnect and retry." : "No active licence. Run 'sproutkit setup' first.";
@@ -96460,7 +96376,7 @@ async function main() {
96460
96376
  say(c.greenBold(" Sproutkit Code"));
96461
96377
  say(" Find leads, build sites and run outreach from your terminal.");
96462
96378
  say();
96463
- say(` ${c.bold("sproutkit")} start the chat`);
96379
+ say(` ${c.bold("sproutkit")} start Claude Code with Sproutkit loaded`);
96464
96380
  say(` ${c.bold('sproutkit find "<area>"')} find leads (--categories "barber shop,cafe")`);
96465
96381
  say(` ${c.bold("sproutkit list")} list leads (--status, --city, --json)`);
96466
96382
  say(` ${c.bold("sproutkit build <id>")} build a website (--instructions, --cms)`);
@@ -96479,7 +96395,8 @@ async function main() {
96479
96395
  say(` ${c.bold('sproutkit "..."')} run a single message and exit`);
96480
96396
  say();
96481
96397
  say(c.dim(" Add --json to find/list/build/outreach for machine-readable output."));
96482
- say(c.dim(" In chat: type a request, Ctrl+C to quit. Actions that spend money ask y/N first."));
96398
+ say(c.dim(" Bare 'sproutkit' hands you to Claude Code \u2014 just ask for what you want."));
96399
+ say(c.dim(" Long runs (build-all, pipeline): press q to stop after in-flight work finishes."));
96483
96400
  say();
96484
96401
  closeTty();
96485
96402
  return;
@@ -96496,109 +96413,46 @@ async function main() {
96496
96413
  closeTty();
96497
96414
  return;
96498
96415
  }
96499
- const bridged = useClaudeBridge();
96500
96416
  const oneShot = process.argv.slice(2).join(" ").trim();
96501
- if (oneShot) {
96502
- if (bridged) {
96503
- const r = await runClaudeTurn({ message: oneShot, sessionId: null, write: (t) => process.stdout.write(t) });
96504
- say();
96505
- if (!r.ok) say(c.red(` ${r.error ?? "Claude Code failed."}`));
96506
- } else {
96507
- await runTurn(oneShot);
96508
- }
96417
+ if (!claudePresent()) {
96418
+ say();
96419
+ say(c.amber(" Claude Code isn't installed \u2014 that's what powers the conversational mode."));
96420
+ say(c.dim(" Install it from https://claude.com/claude-code, then run 'claude /login'."));
96421
+ say(c.dim(" Meanwhile every Sproutkit command still works directly:"));
96422
+ say(` ${c.bold('sproutkit find "Leeds" --categories "barber shop"')}`);
96423
+ say(` ${c.bold("sproutkit list")} \xB7 ${c.bold("sproutkit build <id>")} \xB7 ${c.bold("sproutkit pipeline x3")}`);
96424
+ say(c.dim(" Run 'sproutkit --help' for the full list, or 'sproutkit doctor' to check setup."));
96425
+ say();
96509
96426
  closeTty();
96510
96427
  return;
96511
96428
  }
96512
- await refreshIfStale(getSetting("anthropic_key")).catch(() => {
96513
- });
96514
- await notifyIfOutdated();
96515
- if (bridged && !skillInstalled()) {
96516
- say(c.amber(" Tip: run 'sproutkit setup' to install the Sproutkit skill so Claude can find/build/book for you."));
96517
- }
96518
- if (bridged) {
96519
- say(c.dim(" Talk to Sproutkit (powered by your Claude subscription). Type a request, or 'exit'."));
96520
- say(c.dim(" It runs the right action precisely. Type '/freemode' for open conversation + custom tweaks."));
96521
- } else {
96522
- say(c.dim(" Chat with Sproutkit. Type a request, or 'exit' to quit."));
96429
+ if (!skillInstalled()) {
96430
+ const res = installSkill();
96431
+ if (res.ok) say(c.dim(" Installed the Sproutkit skill for Claude Code."));
96432
+ else say(c.amber(` Couldn't install the Sproutkit skill: ${res.error}`));
96523
96433
  }
96434
+ await notifyIfOutdated();
96524
96435
  say();
96525
- let claudeSession = null;
96526
- let freeMode = false;
96527
- while (true) {
96528
- const line = await ask(c.greenBold("sproutkit \u203A "));
96529
- if (!line) continue;
96530
- if (line === "exit" || line === "quit") break;
96531
- if (bridged && (line === "/freemode" || line === "/free")) {
96532
- freeMode = true;
96533
- say(c.amber(" Free mode ON \u2014 open conversation. You can ask for small custom changes. '/taskmode' to switch back."));
96534
- say();
96535
- continue;
96536
- }
96537
- if (bridged && (line === "/taskmode" || line === "/task")) {
96538
- freeMode = false;
96539
- say(c.green(" Task mode ON \u2014 precise actions."));
96540
- say();
96541
- continue;
96542
- }
96543
- const direct = line.replace(/^\//, "");
96544
- const verb = direct.split(/\s+/)[0]?.toLowerCase();
96545
- const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "pipeline", "doctor"];
96546
- if (verb === "commands" || verb === "help" || line === "/help" || line === "/?") {
96547
- say();
96548
- say(c.bold(" Commands (type with or without a leading /):"));
96549
- say(` ${c.greenBold("find")} "<area>" --categories "cafe,barber" find website-less leads`);
96550
- say(` ${c.greenBold("list")} [--status <s>] [--city <c>] list leads (get ids)`);
96551
- say(` ${c.greenBold("build")} <id> [--instructions "\u2026"] [--cms] [--multipage] build a demo site`);
96552
- say(` ${c.greenBold("build-all")} [--ids 3,5] [--concurrency 3] build many at once`);
96553
- say(` ${c.greenBold("regenerate")} <id> --instructions "\u2026" rebuild a site with changes`);
96554
- say(` ${c.greenBold("cms")} <id> add the editable CMS to a built site`);
96555
- say(` ${c.greenBold("book")} <id> [--standalone] [--archetype slots|reservation|enquiry] [--email \u2026]`);
96556
- say(` ${c.dim(" ")} booking on the site, or --standalone for its own URL`);
96557
- say(` ${c.greenBold("outreach")} [--ids 3,5] prepare outreach links`);
96558
- say(` ${c.greenBold("pipeline")} x3 [--book] [--limit 10] build + outreach many, concurrently`);
96559
- say(c.dim(" doctor \u2014 check your setup \xB7 /freemode \u2014 open chat \xB7 /taskmode \u2014 precise \xB7 exit"));
96560
- say(c.dim(" Or just talk: \u201Cfind cafes in Leeds and build the best two\u201D."));
96561
- say();
96562
- continue;
96563
- }
96564
- const isSlash = line.startsWith("/");
96565
- if (DIRECT_VERBS.includes(verb) && (isSlash || !freeMode)) {
96566
- const argv = (direct.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).slice(1).map((a) => a.replace(/^['"]|['"]$/g, ""));
96567
- try {
96568
- if (verb === "find") await cmdFind(argv);
96569
- else if (verb === "list") await cmdList(argv);
96570
- else if (verb === "build") await cmdBuild(argv);
96571
- else if (verb === "build-all") await cmdBuildAll(argv);
96572
- else if (verb === "regenerate") await cmdRegenerate(argv);
96573
- else if (verb === "cms") await cmdCms(argv);
96574
- else if (verb === "book") await cmdBook(argv);
96575
- else if (verb === "outreach") await cmdOutreach(argv);
96576
- else if (verb === "pipeline") await cmdPipeline(argv);
96577
- else if (verb === "doctor") await runDoctor();
96578
- } catch (e) {
96579
- say(c.red(` Error: ${e.message}`));
96580
- }
96581
- continue;
96582
- }
96583
- try {
96584
- if (bridged) {
96585
- const r = await runClaudeTurn({
96586
- message: line,
96587
- sessionId: claudeSession,
96588
- freeMode,
96589
- write: (t) => process.stdout.write(t)
96590
- });
96591
- say();
96592
- if (r.sessionId) claudeSession = r.sessionId;
96593
- if (!r.ok) say(c.red(` ${r.error ?? "Claude Code failed."}`));
96594
- } else {
96595
- await runTurn(line);
96596
- }
96597
- } catch (e) {
96598
- say(c.red(` Error: ${e.message}`));
96599
- }
96600
- }
96436
+ say(c.greenBold(" Sproutkit") + c.dim(" \u2014 starting Claude Code"));
96437
+ say(c.dim(" Ask for what you want: \u201Cfind barbers in Leeds and build the best two\u201D."));
96438
+ say(c.dim(" Commands still work directly too: sproutkit list \xB7 build <id> \xB7 pipeline x3"));
96439
+ say();
96440
+ const claudeArgs = [
96441
+ "--append-system-prompt",
96442
+ TASK_SYSTEM_PROMPT,
96443
+ "--allowedTools",
96444
+ "Bash(sproutkit *)"
96445
+ ];
96446
+ if (oneShot) claudeArgs.push(oneShot);
96601
96447
  closeTty();
96448
+ const child = (0, import_child_process4.spawn)(claudeBin(), claudeArgs, { stdio: "inherit" });
96449
+ child.on("error", (e) => {
96450
+ say(c.red(` Couldn't start Claude Code: ${e.message}`));
96451
+ process.exitCode = 1;
96452
+ });
96453
+ child.on("exit", (code) => {
96454
+ process.exitCode = code ?? 0;
96455
+ });
96602
96456
  }
96603
96457
  main().catch((e) => {
96604
96458
  say(c.red(`Fatal: ${e.message}`));
@@ -0,0 +1,46 @@
1
+ # Sproutkit
2
+
3
+ You're working with Sproutkit: a licensed CLI that finds local businesses with no
4
+ website, builds them demo sites, adds booking/CMS, and runs outreach. The operator
5
+ talks to you; you drive the `sproutkit` command.
6
+
7
+ Generation runs on the operator's own Claude subscription (or their API key), so
8
+ you don't need any key — just run the commands.
9
+
10
+ ## Commands
11
+
12
+ Always pass `--json` and parse it, then report tersely.
13
+
14
+ | Command | What it does |
15
+ |---|---|
16
+ | `sproutkit find "<area>" --categories "cafe,barber"` | Find businesses with no website. `--max`, `--radius` (default 10km). |
17
+ | `sproutkit list` | List leads with ids. `--status`, `--city`. Get ids here — never guess. |
18
+ | `sproutkit build <id>` | Build + deploy a demo site. `--instructions "…"`, `--cms`, `--multipage`, `--skip-qa`. |
19
+ | `sproutkit build-all` | Build many concurrently. `--ids`, `--concurrency` (max 4). |
20
+ | `sproutkit regenerate <id> --instructions "…"` | Rebuild an existing site with changes. |
21
+ | `sproutkit cms <id>` | Add the editable CMS on top of a site we built. |
22
+ | `sproutkit book <id>` | Booking form on the site. `--standalone` = its own URL (client already has a site). `--archetype slots\|reservation\|enquiry`. |
23
+ | `sproutkit outreach` | Per lead: phone → a messaging link to open; no phone → finds an email and sends it. |
24
+ | `sproutkit pipeline x3` | Full pipeline (build → booking → outreach) for many clients concurrently. `--book`, `--limit`. |
25
+ | `sproutkit doctor` | Check keys/licence/backend are wired up. Run this first when something fails. |
26
+
27
+ ## How to work
28
+
29
+ - Run the command; don't narrate a plan or explore the repo first.
30
+ - Report results as terse lines — `#<id> <name> · <type>` — not prose. Don't
31
+ speculate about why a number is low unless asked.
32
+ - When a request is missing something the command needs, ask one short question
33
+ with the options listed. For a booking site you must establish the type
34
+ (slot-based / reservation / enquiry) and whether it's standalone.
35
+ - Bulk work: use `build-all` or `pipeline`, not many parallel `build` calls, and
36
+ don't spawn subagents for it — those commands own the concurrency and keep it
37
+ inside the operator's rate limits.
38
+ - Long runs can be stopped with `q`; in-flight builds finish, queued ones don't start.
39
+ - `{"ok":false,"error":…}` → read the error and act: "already has a website" →
40
+ offer `regenerate`; missing Blob token → tell them to run `sproutkit setup`.
41
+
42
+ ## Boundaries
43
+
44
+ Sproutkit covers leads, sites (with CMS/booking) and outreach. It can't run ads,
45
+ post to social, phone people or take payment. Only do what was asked — building
46
+ sites and sending outreach spend the operator's money and act in their name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.19.0",
3
+ "version": "1.1.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"