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 +428 -574
- package/dist/skill/CLAUDE.md +46 -0
- package/package.json +1 -1
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
|
|
69075
|
+
const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch2;
|
|
69076
69076
|
const { timesInvoked, times } = mockDispatch2;
|
|
69077
|
-
mockDispatch2.consumed = !
|
|
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
|
|
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:
|
|
69502
|
+
Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
|
|
69503
69503
|
Invocations: timesInvoked,
|
|
69504
|
-
Remaining:
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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
|
-
|
|
80056
|
-
|
|
80057
|
-
|
|
80058
|
-
|
|
80059
|
-
|
|
80060
|
-
|
|
80061
|
-
|
|
80062
|
-
|
|
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
|
|
80070
|
-
${pageList.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}
|
|
80071
|
-
|
|
80072
|
-
|
|
80073
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
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: "
|
|
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
|
-
{
|
|
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/
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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("
|
|
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 (
|
|
96502
|
-
|
|
96503
|
-
|
|
96504
|
-
|
|
96505
|
-
|
|
96506
|
-
|
|
96507
|
-
|
|
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
|
-
|
|
96513
|
-
|
|
96514
|
-
|
|
96515
|
-
|
|
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
|
-
|
|
96526
|
-
|
|
96527
|
-
|
|
96528
|
-
|
|
96529
|
-
|
|
96530
|
-
|
|
96531
|
-
|
|
96532
|
-
|
|
96533
|
-
|
|
96534
|
-
|
|
96535
|
-
|
|
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.
|