sproutkit-code 0.19.0 → 1.0.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);
@@ -80049,28 +80049,84 @@ Whichever you pick, make it FEEL premium and intentional \u2014 not a template.
80049
80049
  - Subtle scroll-revealed details (not heavy animations)
80050
80050
  - 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
80051
 
80052
+ The direction governs LOOK; the archetype below governs STRUCTURE. Vary both. Also
80053
+ vary the mechanics between sites so they don't feel stamped from one mould \u2014 pick
80054
+ what suits this business rather than defaulting:
80055
+ - Section rhythm: alternating full-bleed bands vs a contained column vs a
80056
+ two-column split that runs down the page.
80057
+ - Content shape: cards vs a plain itemised list vs a table vs numbered steps vs a
80058
+ single long editorial column.
80059
+ - Where the accent lands, whether section backgrounds alternate, whether headings
80060
+ are centred or hard-left, whether the page is wide or narrow.
80061
+
80052
80062
  \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)
80063
+ STRUCTURE \u2014 design the page around THIS business, do not follow a fixed template
80054
80064
  \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
80065
+ CRITICAL: sites for different kinds of business must not share a skeleton. What a
80066
+ customer needs from a takeaway is not what they need from a plumber. Decide the
80067
+ section set, their ORDER, and how much space each gets based on what actually
80068
+ drives a decision for a "${lead.type}" \u2014 then commit to it.
80069
+
80070
+ Pick the LAYOUT ARCHETYPE that fits this business (or compose your own if none fits):
80071
+
80072
+ A. MENU / PRODUCT-FORWARD \u2014 food & drink (restaurants, cafes, takeaways, bakeries).
80073
+ The menu IS the page. Lead with it high up, itemised with prices, grouped by
80074
+ course/category. Hero can be compact (a band, not full-screen) to get the menu
80075
+ above the fold. Photos of food used large. Hours + "how to order/book" near top.
80076
+
80077
+ B. BOOKING-FORWARD \u2014 appointment trades (barbers, salons, nails, clinics, dentists).
80078
+ The job is "when can I get in": services + prices as a scannable price list,
80079
+ opening hours prominent, a persistent Book/Call action. Staff or before/after
80080
+ imagery if available. Reviews used as reassurance near the booking action.
80081
+
80082
+ C. TRUST & COVERAGE-FORWARD \u2014 home/field trades (plumbers, electricians, builders,
80083
+ roofers, cleaners, car washes, removals). Buyers need confidence + "do you cover
80084
+ me?". Lead with the problem you solve and credentials (years, insured, callout
80085
+ time), then areas covered, then services, then proof (reviews). Phone must be
80086
+ impossible to miss \u2014 a sticky call bar is appropriate. Photos are of work done.
80087
+
80088
+ D. SHOWCASE / PORTFOLIO-FORWARD \u2014 visual businesses (florists, photographers,
80089
+ tattoo, interiors, boutiques). Imagery dominates: full-bleed gallery early and
80090
+ large, minimal copy, the work sells itself. Contact is quiet but easy.
80091
+
80092
+ E. CREDIBILITY-FORWARD \u2014 professional services (accountants, solicitors, agencies,
80093
+ consultants, tutors). Lead with clarity of offer and who it's for, then process
80094
+ or specialisms, then credentials/testimonials. Restrained, text-led, confident.
80095
+
80096
+ RULES for structure:
80097
+ - Use the archetype's priorities to choose and ORDER sections. Genuinely differ:
80098
+ a takeaway should NOT open with a full-screen hero then "About"; a plumber should
80099
+ NOT bury the phone number under a gallery.
80100
+ - OMIT any section you have no real content for. Never render an empty or padded
80101
+ section \u2014 fewer, fuller sections always beat a complete-looking skeleton.
80102
+ - Vary the hero treatment to suit: full-viewport image, compact banner, split
80103
+ text/image, or type-only. Not every site gets the same big centred hero.
80104
+ - Include a NAV appropriate to the site (sticky if the page is long) with the
80105
+ business name and the single most useful action for THIS business type
80106
+ (tel:${lead.phone} for trades, book for appointments, menu/order for food).
80107
+ - Non-negotiable floor, however you arrange it: the business name, what they do,
80108
+ where they are (${lead.city}), the phone as a tel: link, and a footer ending with
80109
+ "Built with Sproutkit \xB7 sproutkit.app" in muted text.
80110
+ - Use the real reviews and hours given above where they earn their place \u2014 not in a
80111
+ fixed slot. Photos: only from PHOTOS, sized per the archetype (see gallery rule:
80112
+ if PHOTOS.length < 2 there is no gallery section at all).
80063
80113
  ${multipage ? `
80064
80114
  \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
80115
  MULTIPAGE (REQUIRED) \u2014 real separate pages, not one long scroll
80066
80116
  \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
80117
  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
80118
 
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.
80119
+ ${pages && pages.length > 0 ? `Pages (one <Route> each), with a persistent NAV linking between them via <Link>:
80120
+ ${pageList.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}` : `Choose the PAGES yourself from what this business actually needs \u2014 do not default
80121
+ to a generic Home/Services/About/Contact set. Base them on the layout archetype
80122
+ below. For example: a takeaway needs Menu (the big one) + Order/Find us; a plumber
80123
+ needs Services + Areas covered + Get a quote; a salon needs Treatments & prices +
80124
+ Book + Gallery; a florist needs Gallery/Shop + Occasions + Contact. 3-5 pages,
80125
+ each with a real reason to exist. Put them in one <Route> each with a persistent
80126
+ NAV linking between them via <Link>.`}
80127
+
80128
+ - 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.
80129
+ - Give each page enough real content to justify itself; if a page would be thin, merge it rather than padding it.
80074
80130
  - 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
80131
  - Nav must be present on every page and highlight the active route. Include the phone/Book CTA in the nav.
80076
80132
  ` : ""}
@@ -80095,6 +80151,14 @@ CMS INTEGRATION (MANDATORY when this brief mentions it)
80095
80151
  \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
80152
  This site is editable by the business owner via a CMS. You MUST wire it up:
80097
80153
 
80154
+ IMPORTANT \u2014 this schema is a CONTENT contract, not a LAYOUT one. The keys below
80155
+ exist so the /admin editor knows what it can edit; they do NOT dictate the page's
80156
+ structure or order. Still follow the layout archetype above: render these fields
80157
+ wherever that structure puts them, give them the weight that business needs (e.g.
80158
+ services as a priced menu high on the page for a takeaway), and omit anything you
80159
+ have no content for. Do not let this schema flatten every CMS site into the same
80160
+ hero \u2192 about \u2192 services \u2192 contact page.
80161
+
80098
80162
  1. At the top of App.jsx, declare a FALLBACK object with all the copy you'd
80099
80163
  normally hardcode:
80100
80164
  const FALLBACK = {
@@ -94597,7 +94661,7 @@ var require_package2 = __commonJS({
94597
94661
  "package.json"(exports2, module2) {
94598
94662
  module2.exports = {
94599
94663
  name: "sproutkit-code",
94600
- version: "0.19.0",
94664
+ version: "1.0.0",
94601
94665
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
94602
94666
  bin: {
94603
94667
  sproutkit: "./dist/index.js"
@@ -95159,6 +95223,235 @@ async function runOnboarding(force = false) {
95159
95223
  return true;
95160
95224
  }
95161
95225
 
95226
+ // src/update.ts
95227
+ var import_child_process2 = require("child_process");
95228
+ var import_util = require("util");
95229
+ var execFileP = (0, import_util.promisify)(import_child_process2.execFile);
95230
+ var PKG = "sproutkit-code";
95231
+ var CURRENT_VERSION = require_package2().version;
95232
+ async function latestOnRegistry() {
95233
+ try {
95234
+ const r = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
95235
+ signal: AbortSignal.timeout(5e3)
95236
+ });
95237
+ if (!r.ok) return null;
95238
+ const j = await r.json();
95239
+ return j.version ?? null;
95240
+ } catch {
95241
+ return null;
95242
+ }
95243
+ }
95244
+ function isNewer(a, b) {
95245
+ const pa = a.split(".").map(Number);
95246
+ const pb = b.split(".").map(Number);
95247
+ for (let i = 0; i < 3; i++) {
95248
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
95249
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
95250
+ }
95251
+ return false;
95252
+ }
95253
+ async function runUpdate() {
95254
+ say(c.dim(` Current version ${CURRENT_VERSION}. Checking npm\u2026`));
95255
+ const latest = await latestOnRegistry();
95256
+ if (!latest) {
95257
+ say(c.amber(" Couldn't reach npm. Check your connection and try again."));
95258
+ return;
95259
+ }
95260
+ if (!isNewer(latest, CURRENT_VERSION)) {
95261
+ say(c.green(` \u2713 You're on the latest version (${CURRENT_VERSION}).`));
95262
+ return;
95263
+ }
95264
+ say(c.dim(` Updating ${CURRENT_VERSION} \u2192 ${latest}\u2026`));
95265
+ try {
95266
+ await execFileP("npm", ["install", "-g", `${PKG}@latest`], { timeout: 12e4 });
95267
+ say(c.green(` \u2713 Updated to ${latest}. Restart sproutkit to use it.`));
95268
+ } catch (e) {
95269
+ say(c.red(` Update failed: ${e.message}`));
95270
+ say(c.dim(` Try manually: npm install -g ${PKG}@latest`));
95271
+ }
95272
+ }
95273
+ async function notifyIfOutdated() {
95274
+ try {
95275
+ const last = Number(getSetting("update_checked_at") ?? 0);
95276
+ if (Date.now() - last < 24 * 60 * 60 * 1e3) return;
95277
+ setSetting("update_checked_at", String(Date.now()));
95278
+ const latest = await latestOnRegistry();
95279
+ if (latest && isNewer(latest, CURRENT_VERSION)) {
95280
+ say(
95281
+ c.amber(
95282
+ ` \u2191 Update available: ${CURRENT_VERSION} \u2192 ${latest}. Run 'sproutkit update'.`
95283
+ )
95284
+ );
95285
+ say();
95286
+ }
95287
+ } catch {
95288
+ }
95289
+ }
95290
+
95291
+ // src/sync.ts
95292
+ var import_os3 = __toESM(require("os"));
95293
+ var import_path2 = __toESM(require("path"));
95294
+ var import_fs2 = __toESM(require("fs"));
95295
+ var COLS = [
95296
+ "place_id",
95297
+ "name",
95298
+ "type",
95299
+ "city",
95300
+ "address",
95301
+ "phone",
95302
+ "website",
95303
+ "rating",
95304
+ "review_count",
95305
+ "lat",
95306
+ "lng",
95307
+ "status",
95308
+ "site_url",
95309
+ "site_built_at",
95310
+ "contacted_at",
95311
+ "notes",
95312
+ "score",
95313
+ "price_level",
95314
+ "business_status"
95315
+ ];
95316
+ function desktopDbPath() {
95317
+ const home = import_os3.default.homedir();
95318
+ const candidates = [];
95319
+ if (process.platform === "darwin") {
95320
+ candidates.push(import_path2.default.join(home, "Library", "Application Support", "Sproutkit", "data", "sproutio.db"));
95321
+ } else if (process.platform === "win32") {
95322
+ const appData = process.env.APPDATA || import_path2.default.join(home, "AppData", "Roaming");
95323
+ candidates.push(import_path2.default.join(appData, "Sproutkit", "data", "sproutio.db"));
95324
+ } else {
95325
+ const cfg = process.env.XDG_CONFIG_HOME || import_path2.default.join(home, ".config");
95326
+ candidates.push(import_path2.default.join(cfg, "Sproutkit", "data", "sproutio.db"));
95327
+ }
95328
+ return candidates.find((p) => import_fs2.default.existsSync(p)) ?? null;
95329
+ }
95330
+ function syncToDesktop(opts = {}) {
95331
+ const dest = desktopDbPath();
95332
+ if (!dest) return { ok: false, reason: "no_desktop_db", inserted: 0, updated: 0 };
95333
+ const cliLeads = getDb().all(
95334
+ `SELECT ${COLS.join(", ")} FROM leads WHERE place_id IS NOT NULL AND place_id != ''`,
95335
+ []
95336
+ );
95337
+ if (cliLeads.length === 0) return { ok: true, inserted: 0, updated: 0 };
95338
+ const { Database } = require("node-sqlite3-wasm");
95339
+ const ddb = new Database(dest);
95340
+ let inserted = 0;
95341
+ let updated = 0;
95342
+ try {
95343
+ for (const lead of cliLeads) {
95344
+ const existing = ddb.get("SELECT id FROM leads WHERE place_id = ?", [lead.place_id]);
95345
+ if (existing) {
95346
+ ddb.run(
95347
+ "UPDATE leads SET status = ?, site_url = ?, site_built_at = ?, score = ? WHERE place_id = ?",
95348
+ [lead.status, lead.site_url, lead.site_built_at, lead.score, lead.place_id].map(
95349
+ (v) => v ?? null
95350
+ )
95351
+ );
95352
+ updated++;
95353
+ } else {
95354
+ const placeholders = COLS.map(() => "?").join(", ");
95355
+ ddb.run(
95356
+ `INSERT INTO leads (${COLS.join(", ")}) VALUES (${placeholders})`,
95357
+ COLS.map((k) => lead[k] ?? null)
95358
+ );
95359
+ inserted++;
95360
+ }
95361
+ }
95362
+ } finally {
95363
+ ddb.close();
95364
+ }
95365
+ if (!opts.quiet) {
95366
+ say(
95367
+ c.green(` \u2713 Synced to desktop: ${inserted} new, ${updated} updated.`)
95368
+ );
95369
+ }
95370
+ return { ok: true, inserted, updated };
95371
+ }
95372
+ function autoSync() {
95373
+ try {
95374
+ const r = syncToDesktop({ quiet: true });
95375
+ if (r.ok && (r.inserted || r.updated)) {
95376
+ say(c.dim(` \u2194 synced to desktop (${r.inserted} new, ${r.updated} updated)`));
95377
+ }
95378
+ } catch {
95379
+ }
95380
+ }
95381
+
95382
+ // src/models-cmd.ts
95383
+ async function runModelsClaudeCode() {
95384
+ const current = getClaudeCodeModel();
95385
+ say();
95386
+ say(c.bold(" Model (Claude subscription):"));
95387
+ say(c.dim(" Uses your Claude Code login \u2014 pick which model builds run on."));
95388
+ CLAUDE_CODE_MODELS.forEach((m, i) => {
95389
+ const active = m.alias === current ? c.green(" \u2190 current") : "";
95390
+ const isDefault = !current && m.alias === "sonnet" ? c.dim(" (CLI default)") : "";
95391
+ say(` ${c.greenBold(String(i + 1).padStart(2))} ${m.label} ${c.dim(m.alias)}${active}${isDefault}`);
95392
+ });
95393
+ say();
95394
+ const pick = await ask(c.green(" Pick a number to switch (or Enter to keep current): "));
95395
+ if (!pick) {
95396
+ say(c.dim(" No change."));
95397
+ return;
95398
+ }
95399
+ const idx = Number(pick) - 1;
95400
+ if (Number.isNaN(idx) || idx < 0 || idx >= CLAUDE_CODE_MODELS.length) {
95401
+ say(c.amber(" Not a valid choice \u2014 no change made."));
95402
+ return;
95403
+ }
95404
+ const chosen = CLAUDE_CODE_MODELS[idx];
95405
+ setSetting("claude_code_model", chosen.alias);
95406
+ say(c.green(` \u2713 Builds will now use ${chosen.alias}.`));
95407
+ if (chosen.alias === "fable") {
95408
+ say(c.dim(" Note: Fable needs 30-day data retention on your Claude account, or it'll error."));
95409
+ }
95410
+ }
95411
+ async function runModels() {
95412
+ if (getBackend() === "claude_code") {
95413
+ await runModelsClaudeCode();
95414
+ return;
95415
+ }
95416
+ const apiKey = getSetting("anthropic_key");
95417
+ if (!apiKey) {
95418
+ say(c.red(" No Anthropic key set. Run 'sproutkit setup' first."));
95419
+ return;
95420
+ }
95421
+ say(c.dim(" Fetching your available models from Anthropic\u2026"));
95422
+ let models2 = listCachedModels();
95423
+ try {
95424
+ models2 = await refreshModels(apiKey);
95425
+ } catch (e) {
95426
+ say(c.amber(` Couldn't refresh from the API (${e.message}).`));
95427
+ say(c.dim(" Showing the last known list instead."));
95428
+ }
95429
+ if (models2.length === 0) {
95430
+ say(c.red(" No models returned. Check your Anthropic key and connection."));
95431
+ return;
95432
+ }
95433
+ const current = getPreferredModel();
95434
+ say();
95435
+ say(c.bold(" Available models:"));
95436
+ models2.forEach((m, i) => {
95437
+ const active = m.id === current ? c.green(" \u2190 current") : "";
95438
+ say(` ${c.greenBold(String(i + 1).padStart(2))} ${m.display_name ?? m.id} ${c.dim(m.id)}${active}`);
95439
+ });
95440
+ say();
95441
+ const pick = await ask(c.green(" Pick a number to switch (or Enter to keep current): "));
95442
+ if (!pick) {
95443
+ say(c.dim(" Kept current model."));
95444
+ return;
95445
+ }
95446
+ const idx = Number(pick) - 1;
95447
+ if (Number.isNaN(idx) || idx < 0 || idx >= models2.length) {
95448
+ say(c.amber(" Not a valid choice \u2014 no change made."));
95449
+ return;
95450
+ }
95451
+ setSetting("claude_model", models2[idx].id);
95452
+ say(c.green(` \u2713 Now using ${models2[idx].display_name ?? models2[idx].id}.`));
95453
+ }
95454
+
95162
95455
  // src/invoke-ipc.ts
95163
95456
  async function requireBlobToken(vercelToken, emit, leadId) {
95164
95457
  const existing = getSetting("blob_token");
@@ -95308,357 +95601,6 @@ function makeInvokeIpc(emit) {
95308
95601
  };
95309
95602
  }
95310
95603
 
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
95604
  // src/commands.ts
95663
95605
  function makeCtx(opts) {
95664
95606
  const emit = (channel, payload) => {
@@ -96128,8 +96070,7 @@ async function cmdBuildAll(argv) {
96128
96070
  });
96129
96071
  }
96130
96072
 
96131
- // src/claude-bridge.ts
96132
- var import_child_process3 = require("child_process");
96073
+ // src/operating-brief.ts
96133
96074
  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
96075
 
96135
96076
  HOW TO ACT \u2014 this is strict:
@@ -96166,99 +96107,9 @@ COMMANDS:
96166
96107
  - sproutkit pipeline x3 --json [--book] [--limit 10] \u2014 full pipeline (build\u2192booking\u2192outreach) for many clients concurrently
96167
96108
 
96168
96109
  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
96110
 
96260
96111
  // src/doctor.ts
96261
- var import_child_process4 = require("child_process");
96112
+ var import_child_process3 = require("child_process");
96262
96113
  function mark(l) {
96263
96114
  return l === "ok" ? c.green("\u2713") : l === "warn" ? c.amber("!") : c.red("\u2717");
96264
96115
  }
@@ -96277,7 +96128,7 @@ async function runDoctor() {
96277
96128
  if (backend === "claude_code") {
96278
96129
  const present = (() => {
96279
96130
  try {
96280
- return (0, import_child_process4.spawnSync)(claudeBin(), ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96131
+ return (0, import_child_process3.spawnSync)(claudeBin(), ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96281
96132
  } catch {
96282
96133
  return false;
96283
96134
  }
@@ -96381,20 +96232,16 @@ async function runDoctor() {
96381
96232
  }
96382
96233
 
96383
96234
  // src/index.ts
96384
- var import_child_process5 = require("child_process");
96235
+ var import_child_process4 = require("child_process");
96385
96236
  process.env.SPROUTKIT_HEADLESS = "1";
96386
96237
  function claudePresent() {
96387
96238
  try {
96388
96239
  const bin = process.env.SPROUTKIT_CLAUDE_BIN || "claude";
96389
- return (0, import_child_process5.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96240
+ return (0, import_child_process4.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
96390
96241
  } catch {
96391
96242
  return false;
96392
96243
  }
96393
96244
  }
96394
- function useClaudeBridge() {
96395
- if (getBackend() === "claude_code") return true;
96396
- return claudePresent() && !getSetting("anthropic_key");
96397
- }
96398
96245
  async function main() {
96399
96246
  const arg = process.argv[2];
96400
96247
  initDatabase();
@@ -96460,7 +96307,7 @@ async function main() {
96460
96307
  say(c.greenBold(" Sproutkit Code"));
96461
96308
  say(" Find leads, build sites and run outreach from your terminal.");
96462
96309
  say();
96463
- say(` ${c.bold("sproutkit")} start the chat`);
96310
+ say(` ${c.bold("sproutkit")} start Claude Code with Sproutkit loaded`);
96464
96311
  say(` ${c.bold('sproutkit find "<area>"')} find leads (--categories "barber shop,cafe")`);
96465
96312
  say(` ${c.bold("sproutkit list")} list leads (--status, --city, --json)`);
96466
96313
  say(` ${c.bold("sproutkit build <id>")} build a website (--instructions, --cms)`);
@@ -96479,7 +96326,8 @@ async function main() {
96479
96326
  say(` ${c.bold('sproutkit "..."')} run a single message and exit`);
96480
96327
  say();
96481
96328
  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."));
96329
+ say(c.dim(" Bare 'sproutkit' hands you to Claude Code \u2014 just ask for what you want."));
96330
+ say(c.dim(" Long runs (build-all, pipeline): press q to stop after in-flight work finishes."));
96483
96331
  say();
96484
96332
  closeTty();
96485
96333
  return;
@@ -96496,109 +96344,46 @@ async function main() {
96496
96344
  closeTty();
96497
96345
  return;
96498
96346
  }
96499
- const bridged = useClaudeBridge();
96500
96347
  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
- }
96348
+ if (!claudePresent()) {
96349
+ say();
96350
+ say(c.amber(" Claude Code isn't installed \u2014 that's what powers the conversational mode."));
96351
+ say(c.dim(" Install it from https://claude.com/claude-code, then run 'claude /login'."));
96352
+ say(c.dim(" Meanwhile every Sproutkit command still works directly:"));
96353
+ say(` ${c.bold('sproutkit find "Leeds" --categories "barber shop"')}`);
96354
+ say(` ${c.bold("sproutkit list")} \xB7 ${c.bold("sproutkit build <id>")} \xB7 ${c.bold("sproutkit pipeline x3")}`);
96355
+ say(c.dim(" Run 'sproutkit --help' for the full list, or 'sproutkit doctor' to check setup."));
96356
+ say();
96509
96357
  closeTty();
96510
96358
  return;
96511
96359
  }
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."));
96360
+ if (!skillInstalled()) {
96361
+ const res = installSkill();
96362
+ if (res.ok) say(c.dim(" Installed the Sproutkit skill for Claude Code."));
96363
+ else say(c.amber(` Couldn't install the Sproutkit skill: ${res.error}`));
96523
96364
  }
96365
+ await notifyIfOutdated();
96524
96366
  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
- }
96367
+ say(c.greenBold(" Sproutkit") + c.dim(" \u2014 starting Claude Code"));
96368
+ say(c.dim(" Ask for what you want: \u201Cfind barbers in Leeds and build the best two\u201D."));
96369
+ say(c.dim(" Commands still work directly too: sproutkit list \xB7 build <id> \xB7 pipeline x3"));
96370
+ say();
96371
+ const claudeArgs = [
96372
+ "--append-system-prompt",
96373
+ TASK_SYSTEM_PROMPT,
96374
+ "--allowedTools",
96375
+ "Bash(sproutkit *)"
96376
+ ];
96377
+ if (oneShot) claudeArgs.push(oneShot);
96601
96378
  closeTty();
96379
+ const child = (0, import_child_process4.spawn)(claudeBin(), claudeArgs, { stdio: "inherit" });
96380
+ child.on("error", (e) => {
96381
+ say(c.red(` Couldn't start Claude Code: ${e.message}`));
96382
+ process.exitCode = 1;
96383
+ });
96384
+ child.on("exit", (code) => {
96385
+ process.exitCode = code ?? 0;
96386
+ });
96602
96387
  }
96603
96388
  main().catch((e) => {
96604
96389
  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.0.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"