sproutkit-code 0.15.0 → 0.17.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 +419 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30557,6 +30557,147 @@ var require_llm = __commonJS({
|
|
|
30557
30557
|
}
|
|
30558
30558
|
});
|
|
30559
30559
|
|
|
30560
|
+
// ../desktop/dist/main/lib/qa.js
|
|
30561
|
+
var require_qa = __commonJS({
|
|
30562
|
+
"../desktop/dist/main/lib/qa.js"(exports2) {
|
|
30563
|
+
"use strict";
|
|
30564
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
30565
|
+
exports2.staticChecks = staticChecks;
|
|
30566
|
+
exports2.reviewWithClaude = reviewWithClaude;
|
|
30567
|
+
exports2.fixIssues = fixIssues;
|
|
30568
|
+
var llm_1 = require_llm();
|
|
30569
|
+
function staticChecks(args) {
|
|
30570
|
+
const { code, photoCount, phone, businessName } = args;
|
|
30571
|
+
const issues = [];
|
|
30572
|
+
const deadHrefs = code.match(/href\s*=\s*["'`]\s*#\s*["'`]/g)?.length ?? 0;
|
|
30573
|
+
if (deadHrefs > 0) {
|
|
30574
|
+
issues.push({
|
|
30575
|
+
severity: "warn",
|
|
30576
|
+
code: "dead_href",
|
|
30577
|
+
detail: `${deadHrefs} placeholder href="#" link(s) \u2014 nav/CTAs should point somewhere real (tel:, /book, or a section id).`
|
|
30578
|
+
});
|
|
30579
|
+
}
|
|
30580
|
+
if (/lorem ipsum|todo:|placeholder text/i.test(code)) {
|
|
30581
|
+
issues.push({
|
|
30582
|
+
severity: "error",
|
|
30583
|
+
code: "placeholder_copy",
|
|
30584
|
+
detail: "Placeholder copy (lorem ipsum / TODO) left in the page."
|
|
30585
|
+
});
|
|
30586
|
+
}
|
|
30587
|
+
if (businessName && !code.includes(businessName.split(" ")[0])) {
|
|
30588
|
+
issues.push({
|
|
30589
|
+
severity: "error",
|
|
30590
|
+
code: "missing_business_name",
|
|
30591
|
+
detail: `The business name ("${businessName}") doesn't appear in the page.`
|
|
30592
|
+
});
|
|
30593
|
+
}
|
|
30594
|
+
if (phone && !code.includes("tel:")) {
|
|
30595
|
+
issues.push({
|
|
30596
|
+
severity: "warn",
|
|
30597
|
+
code: "missing_phone_cta",
|
|
30598
|
+
detail: "A phone number is on file but there's no tel: link to call it."
|
|
30599
|
+
});
|
|
30600
|
+
}
|
|
30601
|
+
const referenced = new Set((code.match(/\/photo(\d+)\.jpg/g) ?? []).map((m) => Number(m.match(/\d+/)[0])));
|
|
30602
|
+
const overRef = [...referenced].filter((n) => n > photoCount);
|
|
30603
|
+
if (overRef.length > 0) {
|
|
30604
|
+
issues.push({
|
|
30605
|
+
severity: "error",
|
|
30606
|
+
code: "broken_image_ref",
|
|
30607
|
+
detail: `References /photo${overRef.join(", /photo")}.jpg but only ${photoCount} photo(s) were shipped \u2014 those images will be broken.`
|
|
30608
|
+
});
|
|
30609
|
+
}
|
|
30610
|
+
if (photoCount > 0 && referenced.size === 0 && !code.includes("PHOTOS")) {
|
|
30611
|
+
issues.push({
|
|
30612
|
+
severity: "warn",
|
|
30613
|
+
code: "unused_photos",
|
|
30614
|
+
detail: `${photoCount} real photo(s) were shipped but the page uses none of them.`
|
|
30615
|
+
});
|
|
30616
|
+
}
|
|
30617
|
+
if (/>\s*(Opening Hours|Hours)\s*<\/[a-z0-9]+>\s*<\/(div|section)>/i.test(code)) {
|
|
30618
|
+
issues.push({
|
|
30619
|
+
severity: "warn",
|
|
30620
|
+
code: "empty_section",
|
|
30621
|
+
detail: "An Opening Hours heading appears with no content under it \u2014 omit the section instead of rendering it empty."
|
|
30622
|
+
});
|
|
30623
|
+
}
|
|
30624
|
+
return issues;
|
|
30625
|
+
}
|
|
30626
|
+
async function reviewWithClaude(args) {
|
|
30627
|
+
const prompt = `You are reviewing a generated single-page React site (App.jsx) for a real local business before it's shown to the owner. Find only genuine DEFECTS \u2014 things that would look broken, unfinished, or wrong to that owner.
|
|
30628
|
+
|
|
30629
|
+
Business: "${args.businessName}" (${args.businessType})
|
|
30630
|
+
|
|
30631
|
+
Look for: sections that render empty or with placeholder content; copy that doesn't match this business/industry; links that go nowhere; text that's cut off mid-sentence; duplicated sections; contact details that contradict each other; obviously broken layout (e.g. a grid with one item, overlapping fixed elements).
|
|
30632
|
+
Do NOT report style preferences, colour choices, or "could be improved" suggestions. Only defects.
|
|
30633
|
+
|
|
30634
|
+
Respond with ONLY compact JSON, no prose:
|
|
30635
|
+
{"issues":[{"severity":"error"|"warn","code":"short_slug","detail":"one sentence"}]}
|
|
30636
|
+
If there are no real defects, return {"issues":[]}.
|
|
30637
|
+
|
|
30638
|
+
App.jsx:
|
|
30639
|
+
\`\`\`jsx
|
|
30640
|
+
${args.code.slice(0, 45e3)}
|
|
30641
|
+
\`\`\``;
|
|
30642
|
+
try {
|
|
30643
|
+
const res = await (0, llm_1.generateText)({
|
|
30644
|
+
apiKey: args.apiKey,
|
|
30645
|
+
prompt,
|
|
30646
|
+
maxTokens: 2048,
|
|
30647
|
+
timeoutMs: 18e4
|
|
30648
|
+
});
|
|
30649
|
+
const raw = res.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
30650
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
30651
|
+
if (!match)
|
|
30652
|
+
return [];
|
|
30653
|
+
const parsed = JSON.parse(match[0]);
|
|
30654
|
+
if (!Array.isArray(parsed.issues))
|
|
30655
|
+
return [];
|
|
30656
|
+
return parsed.issues.filter((i) => !!i && typeof i.detail === "string").slice(0, 12).map((i) => ({
|
|
30657
|
+
severity: i.severity === "error" ? "error" : "warn",
|
|
30658
|
+
code: String(i.code || "issue").slice(0, 40),
|
|
30659
|
+
detail: String(i.detail).slice(0, 300)
|
|
30660
|
+
}));
|
|
30661
|
+
} catch {
|
|
30662
|
+
return [];
|
|
30663
|
+
}
|
|
30664
|
+
}
|
|
30665
|
+
async function fixIssues(args) {
|
|
30666
|
+
if (args.issues.length === 0)
|
|
30667
|
+
return null;
|
|
30668
|
+
const list = args.issues.map((i, n) => `${n + 1}. [${i.severity}] ${i.detail}`).join("\n");
|
|
30669
|
+
const prompt = `Fix these specific defects in the React component below. Change ONLY what's needed to fix them \u2014 do not redesign, restyle, rewrite copy that isn't faulty, or remove working sections.
|
|
30670
|
+
|
|
30671
|
+
Defects:
|
|
30672
|
+
${list}
|
|
30673
|
+
|
|
30674
|
+
Rules: keep the same design language and structure; if a section can't be filled with real content, remove the section entirely rather than leaving it empty; never invent image URLs (only use the existing PHOTOS entries); the file must remain a single component ending in \`export default App;\`.
|
|
30675
|
+
|
|
30676
|
+
Return ONLY the complete corrected App.jsx \u2014 no markdown fences, no commentary.
|
|
30677
|
+
|
|
30678
|
+
\`\`\`jsx
|
|
30679
|
+
${args.code.slice(0, 45e3)}
|
|
30680
|
+
\`\`\``;
|
|
30681
|
+
try {
|
|
30682
|
+
const res = await (0, llm_1.generateText)({
|
|
30683
|
+
apiKey: args.apiKey,
|
|
30684
|
+
prompt,
|
|
30685
|
+
maxTokens: 64e3,
|
|
30686
|
+
timeoutMs: 3e5
|
|
30687
|
+
});
|
|
30688
|
+
const cleaned = res.text.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, "").trim();
|
|
30689
|
+
if (!cleaned.includes("export default App"))
|
|
30690
|
+
return null;
|
|
30691
|
+
if (cleaned.length < args.code.length * 0.5)
|
|
30692
|
+
return null;
|
|
30693
|
+
return cleaned;
|
|
30694
|
+
} catch {
|
|
30695
|
+
return null;
|
|
30696
|
+
}
|
|
30697
|
+
}
|
|
30698
|
+
}
|
|
30699
|
+
});
|
|
30700
|
+
|
|
30560
30701
|
// ../desktop/dist/main/lib/models.js
|
|
30561
30702
|
var require_models = __commonJS({
|
|
30562
30703
|
"../desktop/dist/main/lib/models.js"(exports2) {
|
|
@@ -35390,7 +35531,7 @@ var require_cross_spawn = __commonJS({
|
|
|
35390
35531
|
enoent.hookChildProcess(spawned, parsed);
|
|
35391
35532
|
return spawned;
|
|
35392
35533
|
}
|
|
35393
|
-
function
|
|
35534
|
+
function spawnSync4(command, args, options) {
|
|
35394
35535
|
const parsed = parse(command, args, options);
|
|
35395
35536
|
const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
|
|
35396
35537
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
@@ -35398,7 +35539,7 @@ var require_cross_spawn = __commonJS({
|
|
|
35398
35539
|
}
|
|
35399
35540
|
module2.exports = spawn2;
|
|
35400
35541
|
module2.exports.spawn = spawn2;
|
|
35401
|
-
module2.exports.sync =
|
|
35542
|
+
module2.exports.sync = spawnSync4;
|
|
35402
35543
|
module2.exports._parse = parse;
|
|
35403
35544
|
module2.exports._enoent = enoent;
|
|
35404
35545
|
}
|
|
@@ -79518,6 +79659,7 @@ var require_builder = __commonJS({
|
|
|
79518
79659
|
var runtime_1 = require_runtime();
|
|
79519
79660
|
var db_1 = require_db();
|
|
79520
79661
|
var llm_1 = require_llm();
|
|
79662
|
+
var qa_1 = require_qa();
|
|
79521
79663
|
var fs3 = __importStar(require("fs"));
|
|
79522
79664
|
var path3 = __importStar(require("path"));
|
|
79523
79665
|
var models_1 = require_models();
|
|
@@ -79760,7 +79902,7 @@ var require_builder = __commonJS({
|
|
|
79760
79902
|
}
|
|
79761
79903
|
}
|
|
79762
79904
|
async function buildAndDeploySite(args) {
|
|
79763
|
-
const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, includeCms, blobToken, reuseAdminPassword, skipContentExtraction, multipage, pages } = args;
|
|
79905
|
+
const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, includeCms, blobToken, reuseAdminPassword, skipContentExtraction, multipage, pages, skipQa } = args;
|
|
79764
79906
|
const pageList = multipage && (!pages || pages.length === 0) ? ["Home", "Services", "About", "Contact"] : pages ?? [];
|
|
79765
79907
|
if (includeCms && !blobToken) {
|
|
79766
79908
|
throw new Error("CMS is enabled but no Vercel Blob token is set. Add it in Settings \u2192 Vercel Blob token.");
|
|
@@ -80073,6 +80215,35 @@ ${prompt}` : prompt;
|
|
|
80073
80215
|
}
|
|
80074
80216
|
}
|
|
80075
80217
|
appCode = appCode.replace("REPLACE_PHOTOS", JSON.stringify(photoPaths));
|
|
80218
|
+
if (!skipQa) {
|
|
80219
|
+
onProgress("Reviewing the site for defects\u2026");
|
|
80220
|
+
const staticIssues = (0, qa_1.staticChecks)({
|
|
80221
|
+
code: appCode,
|
|
80222
|
+
photoCount: photoFiles.length,
|
|
80223
|
+
phone: lead.phone ?? "",
|
|
80224
|
+
businessName: lead.name
|
|
80225
|
+
});
|
|
80226
|
+
const modelIssues = await (0, qa_1.reviewWithClaude)({
|
|
80227
|
+
code: appCode,
|
|
80228
|
+
businessName: lead.name,
|
|
80229
|
+
businessType: lead.type ?? "local business",
|
|
80230
|
+
apiKey: anthropicKey
|
|
80231
|
+
});
|
|
80232
|
+
const issues = [...staticIssues, ...modelIssues];
|
|
80233
|
+
if (issues.length > 0) {
|
|
80234
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
80235
|
+
onProgress(`QA found ${issues.length} issue${issues.length === 1 ? "" : "s"}${errors ? ` (${errors} serious)` : ""} \u2014 fixing\u2026`);
|
|
80236
|
+
const fixed = await (0, qa_1.fixIssues)({ code: appCode, issues, apiKey: anthropicKey });
|
|
80237
|
+
if (fixed) {
|
|
80238
|
+
appCode = fixed;
|
|
80239
|
+
onProgress("QA fixes applied.");
|
|
80240
|
+
} else {
|
|
80241
|
+
onProgress("QA fix didn't come back clean \u2014 deploying the original.");
|
|
80242
|
+
}
|
|
80243
|
+
} else {
|
|
80244
|
+
onProgress("QA passed.");
|
|
80245
|
+
}
|
|
80246
|
+
}
|
|
80076
80247
|
onProgress("Deploying to Vercel...");
|
|
80077
80248
|
const indexHtml = `<!DOCTYPE html>
|
|
80078
80249
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${lead.name}</title></head>
|
|
@@ -92713,6 +92884,77 @@ var require_email = __commonJS({
|
|
|
92713
92884
|
}
|
|
92714
92885
|
});
|
|
92715
92886
|
|
|
92887
|
+
// ../desktop/dist/main/lib/email-harvest.js
|
|
92888
|
+
var require_email_harvest = __commonJS({
|
|
92889
|
+
"../desktop/dist/main/lib/email-harvest.js"(exports2) {
|
|
92890
|
+
"use strict";
|
|
92891
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
92892
|
+
exports2.harvestEmail = harvestEmail;
|
|
92893
|
+
var llm_1 = require_llm();
|
|
92894
|
+
var BAD_EMAIL_DOMAINS = [
|
|
92895
|
+
"yelp.com",
|
|
92896
|
+
"yell.com",
|
|
92897
|
+
"tripadvisor",
|
|
92898
|
+
"facebook.com",
|
|
92899
|
+
"instagram.com",
|
|
92900
|
+
"google.com",
|
|
92901
|
+
"justeat",
|
|
92902
|
+
"deliveroo",
|
|
92903
|
+
"ubereats",
|
|
92904
|
+
"treatwell",
|
|
92905
|
+
"fresha.com",
|
|
92906
|
+
"booking.com",
|
|
92907
|
+
"example.com",
|
|
92908
|
+
"sentry.io",
|
|
92909
|
+
"wixpress.com",
|
|
92910
|
+
"squarespace.com"
|
|
92911
|
+
];
|
|
92912
|
+
function plausibleEmail(raw) {
|
|
92913
|
+
if (typeof raw !== "string")
|
|
92914
|
+
return null;
|
|
92915
|
+
const email = raw.trim().toLowerCase();
|
|
92916
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email))
|
|
92917
|
+
return null;
|
|
92918
|
+
const domain = email.split("@")[1] ?? "";
|
|
92919
|
+
if (BAD_EMAIL_DOMAINS.some((d) => domain.includes(d)))
|
|
92920
|
+
return null;
|
|
92921
|
+
return email;
|
|
92922
|
+
}
|
|
92923
|
+
async function harvestEmail(args) {
|
|
92924
|
+
const prompt = `Search the web for the contact email address of this specific local business.
|
|
92925
|
+
|
|
92926
|
+
Business: "${args.name}"
|
|
92927
|
+
Address: ${args.address}${args.city ? `, ${args.city}` : ""}
|
|
92928
|
+
|
|
92929
|
+
Rules \u2014 be strict:
|
|
92930
|
+
- The email must clearly belong to THIS business at THIS address/city. A business with the same or similar name in a different town or country is NOT a match \u2014 return null.
|
|
92931
|
+
- Prefer an email on the business's own website or its official social profile.
|
|
92932
|
+
- Do NOT return an email belonging to a directory, aggregator or booking platform (Yelp, Yell, TripAdvisor, JustEat, Deliveroo, Treatwell, Fresha, etc.).
|
|
92933
|
+
- Do NOT guess, construct, or infer an address (e.g. never invent info@<name>.com).
|
|
92934
|
+
- If you cannot confirm a real email for this exact business, return null.
|
|
92935
|
+
|
|
92936
|
+
Respond with ONLY compact JSON, no prose: {"email": string|null}`;
|
|
92937
|
+
try {
|
|
92938
|
+
const res = await (0, llm_1.generateText)({
|
|
92939
|
+
apiKey: args.apiKey,
|
|
92940
|
+
prompt,
|
|
92941
|
+
allowWebSearch: true,
|
|
92942
|
+
maxTokens: 512,
|
|
92943
|
+
timeoutMs: 9e4
|
|
92944
|
+
});
|
|
92945
|
+
const raw = res.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
92946
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
92947
|
+
if (!match)
|
|
92948
|
+
return null;
|
|
92949
|
+
const parsed = JSON.parse(match[0]);
|
|
92950
|
+
return plausibleEmail(parsed.email);
|
|
92951
|
+
} catch {
|
|
92952
|
+
return null;
|
|
92953
|
+
}
|
|
92954
|
+
}
|
|
92955
|
+
}
|
|
92956
|
+
});
|
|
92957
|
+
|
|
92716
92958
|
// ../desktop/dist/main/lib/chat-tools.js
|
|
92717
92959
|
var require_chat_tools = __commonJS({
|
|
92718
92960
|
"../desktop/dist/main/lib/chat-tools.js"(exports2) {
|
|
@@ -92726,6 +92968,7 @@ var require_chat_tools = __commonJS({
|
|
|
92726
92968
|
var lead_search_1 = require_lead_search();
|
|
92727
92969
|
var places_1 = require_places();
|
|
92728
92970
|
var email_1 = require_email();
|
|
92971
|
+
var email_harvest_1 = require_email_harvest();
|
|
92729
92972
|
exports2.TOOLS = [
|
|
92730
92973
|
{
|
|
92731
92974
|
name: "find_leads",
|
|
@@ -92846,6 +93089,10 @@ var require_chat_tools = __commonJS({
|
|
|
92846
93089
|
multipage: {
|
|
92847
93090
|
type: "boolean",
|
|
92848
93091
|
description: "When true, build a MULTI-PAGE site (Home/Services/About/Contact as real routed pages with a nav) instead of a single long-scroll page. Use when the user asks for a multipage site or a bigger site with separate pages."
|
|
93092
|
+
},
|
|
93093
|
+
skipQa: {
|
|
93094
|
+
type: "boolean",
|
|
93095
|
+
description: "Skip the post-build QA review (a review + fix pass that catches broken links, empty sections, broken image refs). Only set true when the user explicitly wants maximum speed/volume over quality."
|
|
92849
93096
|
}
|
|
92850
93097
|
},
|
|
92851
93098
|
required: ["id"]
|
|
@@ -92975,6 +93222,7 @@ var require_chat_tools = __commonJS({
|
|
|
92975
93222
|
customInstructions: args.instructions,
|
|
92976
93223
|
includeCms: args.includeCms,
|
|
92977
93224
|
multipage: args.multipage,
|
|
93225
|
+
skipQa: args.skipQa,
|
|
92978
93226
|
blobToken,
|
|
92979
93227
|
onProgress: (msg) => args.ctx.emitProgress?.({
|
|
92980
93228
|
channel: "sites:progress",
|
|
@@ -93160,7 +93408,7 @@ Interested?`;
|
|
|
93160
93408
|
}
|
|
93161
93409
|
case "prepare_outreach": {
|
|
93162
93410
|
const explicit = input.ids;
|
|
93163
|
-
const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, site_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all("SELECT id, name, phone, email, site_url FROM leads WHERE status = 'site_built' AND site_url IS NOT NULL AND site_url != '' ORDER BY score DESC NULLS LAST, id DESC", []);
|
|
93411
|
+
const rows = explicit && explicit.length > 0 ? db2.all(`SELECT id, name, phone, email, address, city, site_url FROM leads WHERE id IN (${explicit.map(() => "?").join(",")})`, explicit) : db2.all("SELECT id, name, phone, email, address, city, site_url FROM leads WHERE status = 'site_built' AND site_url IS NOT NULL AND site_url != '' ORDER BY score DESC NULLS LAST, id DESC", []);
|
|
93164
93412
|
const eligible = rows.filter((r) => r.site_url);
|
|
93165
93413
|
if (eligible.length === 0) {
|
|
93166
93414
|
return {
|
|
@@ -93174,9 +93422,27 @@ Interested?`;
|
|
|
93174
93422
|
const smtp = (0, email_1.smtpFromSettings)((k) => getDecryptedSetting(k));
|
|
93175
93423
|
const emailSubject = getDecryptedSetting("outreach_email_subject") || "A free demo website for {name}";
|
|
93176
93424
|
const prepared = [];
|
|
93425
|
+
let harvested = 0;
|
|
93177
93426
|
for (const r of eligible) {
|
|
93178
93427
|
const message = template.replace(/\{name\}/g, r.name).replace(/\{site_url\}/g, r.site_url ?? "");
|
|
93179
93428
|
const e164 = toE164(r.phone ?? "");
|
|
93429
|
+
if (!e164 && !r.email && smtp) {
|
|
93430
|
+
ctx.emitProgress?.({
|
|
93431
|
+
channel: "outreach:progress",
|
|
93432
|
+
payload: { msg: `Looking up an email for ${r.name}\u2026` }
|
|
93433
|
+
});
|
|
93434
|
+
const found = await (0, email_harvest_1.harvestEmail)({
|
|
93435
|
+
name: r.name,
|
|
93436
|
+
address: r.address ?? "",
|
|
93437
|
+
city: r.city ?? "",
|
|
93438
|
+
apiKey: getDecryptedSetting("anthropic_key")
|
|
93439
|
+
});
|
|
93440
|
+
if (found) {
|
|
93441
|
+
db2.run("UPDATE leads SET email = ? WHERE id = ?", [found, r.id]);
|
|
93442
|
+
r.email = found;
|
|
93443
|
+
harvested++;
|
|
93444
|
+
}
|
|
93445
|
+
}
|
|
93180
93446
|
if (e164) {
|
|
93181
93447
|
prepared.push({
|
|
93182
93448
|
id: r.id,
|
|
@@ -93227,6 +93493,7 @@ Interested?`;
|
|
|
93227
93493
|
phoneLinks: prepared.filter((p) => p.via === "phone").length,
|
|
93228
93494
|
emailsSent,
|
|
93229
93495
|
emailsFailed,
|
|
93496
|
+
emailsFound: harvested,
|
|
93230
93497
|
unreachable,
|
|
93231
93498
|
// Guide the user if email-only leads were skipped for want of SMTP.
|
|
93232
93499
|
note: smtpMissing ? "Some leads have only an email but SMTP isn't set up \u2014 run 'sproutkit setup' to add SMTP and email them." : void 0,
|
|
@@ -93248,6 +93515,7 @@ Interested?`;
|
|
|
93248
93515
|
instructions: input.instructions,
|
|
93249
93516
|
includeCms: !!input.includeCms,
|
|
93250
93517
|
multipage: !!input.multipage,
|
|
93518
|
+
skipQa: input.skipQa === true,
|
|
93251
93519
|
ctx
|
|
93252
93520
|
});
|
|
93253
93521
|
return { ...result, id };
|
|
@@ -93265,6 +93533,7 @@ Interested?`;
|
|
|
93265
93533
|
id,
|
|
93266
93534
|
instructions,
|
|
93267
93535
|
multipage: !!input.multipage,
|
|
93536
|
+
skipQa: input.skipQa === true,
|
|
93268
93537
|
ctx
|
|
93269
93538
|
});
|
|
93270
93539
|
return { ...result, id };
|
|
@@ -94328,7 +94597,7 @@ var require_package2 = __commonJS({
|
|
|
94328
94597
|
"package.json"(exports2, module2) {
|
|
94329
94598
|
module2.exports = {
|
|
94330
94599
|
name: "sproutkit-code",
|
|
94331
|
-
version: "0.
|
|
94600
|
+
version: "0.17.0",
|
|
94332
94601
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
94333
94602
|
bin: {
|
|
94334
94603
|
sproutkit: "./dist/index.js"
|
|
@@ -94589,10 +94858,10 @@ var Status = class {
|
|
|
94589
94858
|
this.timer = null;
|
|
94590
94859
|
}
|
|
94591
94860
|
const label = finalLabel ?? this.label;
|
|
94592
|
-
const
|
|
94593
|
-
if (this.tty) process.stdout.write(`\r\x1B[2K ${
|
|
94861
|
+
const mark2 = ok ? c.green("\u2713") : c.red("\u2717");
|
|
94862
|
+
if (this.tty) process.stdout.write(`\r\x1B[2K ${mark2} ${label}
|
|
94594
94863
|
`);
|
|
94595
|
-
else say(` ${
|
|
94864
|
+
else say(` ${mark2} ${label}`);
|
|
94596
94865
|
}
|
|
94597
94866
|
};
|
|
94598
94867
|
var STATE_MARK = {
|
|
@@ -94641,16 +94910,16 @@ var StatusBoard = class {
|
|
|
94641
94910
|
else this.render();
|
|
94642
94911
|
}
|
|
94643
94912
|
line(r) {
|
|
94644
|
-
const
|
|
94913
|
+
const mark2 = r.state === "building" ? c.cyan(FRAMES[this.frame]) : STATE_MARK[r.state]();
|
|
94645
94914
|
const detail = r.detail ? c.dim(` \xB7 ${r.detail}`) : "";
|
|
94646
|
-
return ` ${
|
|
94915
|
+
return ` ${mark2} ${r.label}${detail}`;
|
|
94647
94916
|
}
|
|
94648
94917
|
logRowIfChanged(r) {
|
|
94649
94918
|
const sig = `${r.state}:${r.detail ?? ""}`;
|
|
94650
94919
|
if (this.lastSig.get(r.key) === sig) return;
|
|
94651
94920
|
this.lastSig.set(r.key, sig);
|
|
94652
|
-
const
|
|
94653
|
-
say(` ${
|
|
94921
|
+
const mark2 = r.state === "built" ? "\u2713" : r.state === "failed" ? "\u2717" : r.state === "skipped" ? "\u2013" : "\u2026";
|
|
94922
|
+
say(` ${mark2} ${r.label}${r.detail ? ` \xB7 ${r.detail}` : ""}`);
|
|
94654
94923
|
}
|
|
94655
94924
|
render() {
|
|
94656
94925
|
if (this.lastLineCount > 0) process.stdout.write(`\x1B[${this.lastLineCount}A`);
|
|
@@ -95485,7 +95754,8 @@ async function cmdBuild(argv) {
|
|
|
95485
95754
|
id,
|
|
95486
95755
|
instructions: flags.instructions ? String(flags.instructions) : void 0,
|
|
95487
95756
|
includeCms: !!flags.cms,
|
|
95488
|
-
multipage: !!flags.multipage
|
|
95757
|
+
multipage: !!flags.multipage,
|
|
95758
|
+
skipQa: !!flags["skip-qa"]
|
|
95489
95759
|
},
|
|
95490
95760
|
makeCtx({ json, status })
|
|
95491
95761
|
);
|
|
@@ -95540,6 +95810,7 @@ async function cmdOutreach(argv) {
|
|
|
95540
95810
|
say();
|
|
95541
95811
|
const parts = [];
|
|
95542
95812
|
if (result.phoneLinks) parts.push(`${result.phoneLinks} message link${result.phoneLinks === 1 ? "" : "s"} to open`);
|
|
95813
|
+
if (result.emailsFound) parts.push(c.dim(`${result.emailsFound} email${result.emailsFound === 1 ? "" : "s"} found`));
|
|
95543
95814
|
if (result.emailsSent) parts.push(`${result.emailsSent} emailed`);
|
|
95544
95815
|
if (result.emailsFailed) parts.push(c.red(`${result.emailsFailed} email failed`));
|
|
95545
95816
|
if (result.unreachable) parts.push(c.amber(`${result.unreachable} unreachable`));
|
|
@@ -95569,7 +95840,7 @@ async function cmdRegenerate(argv) {
|
|
|
95569
95840
|
try {
|
|
95570
95841
|
const result = await runTool(
|
|
95571
95842
|
"regenerate_site",
|
|
95572
|
-
{ id, instructions, multipage: !!flags.multipage },
|
|
95843
|
+
{ id, instructions, multipage: !!flags.multipage, skipQa: !!flags["skip-qa"] },
|
|
95573
95844
|
makeCtx({ json, status })
|
|
95574
95845
|
);
|
|
95575
95846
|
if (result && result.ok === false) {
|
|
@@ -95845,13 +96116,136 @@ function runClaudeTurn(args) {
|
|
|
95845
96116
|
});
|
|
95846
96117
|
}
|
|
95847
96118
|
|
|
95848
|
-
// src/
|
|
96119
|
+
// src/doctor.ts
|
|
95849
96120
|
var import_child_process4 = require("child_process");
|
|
96121
|
+
function mark(l) {
|
|
96122
|
+
return l === "ok" ? c.green("\u2713") : l === "warn" ? c.amber("!") : c.red("\u2717");
|
|
96123
|
+
}
|
|
96124
|
+
async function runDoctor() {
|
|
96125
|
+
const rows = [];
|
|
96126
|
+
const backend = getBackend();
|
|
96127
|
+
const lic = await check();
|
|
96128
|
+
rows.push(
|
|
96129
|
+
lic.status === "active" ? { level: "ok", label: "Licence", detail: "active" } : {
|
|
96130
|
+
level: "fail",
|
|
96131
|
+
label: "Licence",
|
|
96132
|
+
detail: lic.status,
|
|
96133
|
+
fix: "Run 'sproutkit setup' and enter your key, or buy one at https://sproutkit.app"
|
|
96134
|
+
}
|
|
96135
|
+
);
|
|
96136
|
+
if (backend === "claude_code") {
|
|
96137
|
+
const present = (() => {
|
|
96138
|
+
try {
|
|
96139
|
+
return (0, import_child_process4.spawnSync)(claudeBin(), ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
|
|
96140
|
+
} catch {
|
|
96141
|
+
return false;
|
|
96142
|
+
}
|
|
96143
|
+
})();
|
|
96144
|
+
if (!present) {
|
|
96145
|
+
rows.push({
|
|
96146
|
+
level: "fail",
|
|
96147
|
+
label: "Claude Code",
|
|
96148
|
+
detail: "CLI not found",
|
|
96149
|
+
fix: "Install Claude Code, run 'claude /login', then 'sproutkit setup'."
|
|
96150
|
+
});
|
|
96151
|
+
} else {
|
|
96152
|
+
const auth = await checkClaudeCodeAuth();
|
|
96153
|
+
rows.push(
|
|
96154
|
+
auth.ok ? {
|
|
96155
|
+
level: "ok",
|
|
96156
|
+
label: "Claude (subscription)",
|
|
96157
|
+
detail: `logged in \xB7 model: ${getClaudeCodeModel() ?? "default"}`
|
|
96158
|
+
} : {
|
|
96159
|
+
level: "fail",
|
|
96160
|
+
label: "Claude (subscription)",
|
|
96161
|
+
detail: auth.reason,
|
|
96162
|
+
fix: "Run 'claude /login' in another terminal, then re-run doctor."
|
|
96163
|
+
}
|
|
96164
|
+
);
|
|
96165
|
+
}
|
|
96166
|
+
} else {
|
|
96167
|
+
rows.push(
|
|
96168
|
+
getSetting("anthropic_key") ? { level: "ok", label: "Anthropic API key", detail: "set" } : {
|
|
96169
|
+
level: "fail",
|
|
96170
|
+
label: "Anthropic API key",
|
|
96171
|
+
detail: "missing",
|
|
96172
|
+
fix: "Run 'sproutkit setup' (or switch to the Claude subscription backend)."
|
|
96173
|
+
}
|
|
96174
|
+
);
|
|
96175
|
+
}
|
|
96176
|
+
rows.push(
|
|
96177
|
+
getSetting("places_key") ? { level: "ok", label: "Google Places key", detail: "set \u2014 lead search works" } : {
|
|
96178
|
+
level: "fail",
|
|
96179
|
+
label: "Google Places key",
|
|
96180
|
+
detail: "missing",
|
|
96181
|
+
fix: "Run 'sproutkit setup'. Without it, 'find' can't search for leads."
|
|
96182
|
+
}
|
|
96183
|
+
);
|
|
96184
|
+
rows.push(
|
|
96185
|
+
getSetting("vercel_token") ? { level: "ok", label: "Vercel token", detail: "set \u2014 deploys work" } : {
|
|
96186
|
+
level: "fail",
|
|
96187
|
+
label: "Vercel token",
|
|
96188
|
+
detail: "missing",
|
|
96189
|
+
fix: "Run 'sproutkit setup'. Without it, sites can't deploy."
|
|
96190
|
+
}
|
|
96191
|
+
);
|
|
96192
|
+
rows.push(
|
|
96193
|
+
getSetting("blob_token") ? { level: "ok", label: "Vercel Blob token", detail: "set \u2014 booking & CMS work" } : {
|
|
96194
|
+
level: "warn",
|
|
96195
|
+
label: "Vercel Blob token",
|
|
96196
|
+
detail: "missing \u2014 booking sites & CMS will fail",
|
|
96197
|
+
fix: "Create a Blob store at https://vercel.com/dashboard/stores, copy BLOB_READ_WRITE_TOKEN, then 'sproutkit setup'. One token covers all your sites."
|
|
96198
|
+
}
|
|
96199
|
+
);
|
|
96200
|
+
const smtpReady = !!(getSetting("smtp_host") && getSetting("smtp_user") && getSetting("smtp_pass"));
|
|
96201
|
+
rows.push(
|
|
96202
|
+
smtpReady ? { level: "ok", label: "SMTP (email outreach)", detail: `via ${getSetting("smtp_host")}` } : {
|
|
96203
|
+
level: "warn",
|
|
96204
|
+
label: "SMTP (email outreach)",
|
|
96205
|
+
detail: "not set \u2014 leads without a phone can't be emailed",
|
|
96206
|
+
fix: "Run 'sproutkit setup' and add SMTP host/user/password."
|
|
96207
|
+
}
|
|
96208
|
+
);
|
|
96209
|
+
rows.push(
|
|
96210
|
+
getSetting("resend_key") ? { level: "ok", label: "Resend key", detail: "set \u2014 booking emails notify the owner" } : {
|
|
96211
|
+
level: "warn",
|
|
96212
|
+
label: "Resend key",
|
|
96213
|
+
detail: "not set \u2014 bookings are stored but the owner isn't emailed",
|
|
96214
|
+
fix: "Optional. Add it in 'sproutkit setup' if you want booking notifications."
|
|
96215
|
+
}
|
|
96216
|
+
);
|
|
96217
|
+
rows.push(
|
|
96218
|
+
skillInstalled() ? { level: "ok", label: "Claude Code skill", detail: skillTargetDir() } : {
|
|
96219
|
+
level: "warn",
|
|
96220
|
+
label: "Claude Code skill",
|
|
96221
|
+
detail: "not installed",
|
|
96222
|
+
fix: "Run 'sproutkit setup' and say yes to installing the skill, so `claude` can drive Sproutkit."
|
|
96223
|
+
}
|
|
96224
|
+
);
|
|
96225
|
+
say();
|
|
96226
|
+
say(c.greenBold(" Sproutkit doctor"));
|
|
96227
|
+
say(c.dim(` Backend: ${backend === "claude_code" ? "Claude subscription" : "Anthropic API key"}`));
|
|
96228
|
+
say();
|
|
96229
|
+
for (const r of rows) {
|
|
96230
|
+
say(` ${mark(r.level)} ${c.bold(r.label)}${r.detail ? c.dim(` \u2014 ${r.detail}`) : ""}`);
|
|
96231
|
+
if (r.fix && r.level !== "ok") say(c.dim(` \u2192 ${r.fix}`));
|
|
96232
|
+
}
|
|
96233
|
+
say();
|
|
96234
|
+
const fails = rows.filter((r) => r.level === "fail").length;
|
|
96235
|
+
const warns = rows.filter((r) => r.level === "warn").length;
|
|
96236
|
+
if (fails === 0 && warns === 0) say(c.green(" All good \u2014 you're ready to find leads and build sites."));
|
|
96237
|
+
else if (fails === 0) say(c.amber(` Ready to build, with ${warns} optional thing${warns === 1 ? "" : "s"} unset (see above).`));
|
|
96238
|
+
else say(c.red(` ${fails} blocking issue${fails === 1 ? "" : "s"} \u2014 fix those first.`));
|
|
96239
|
+
say();
|
|
96240
|
+
}
|
|
96241
|
+
|
|
96242
|
+
// src/index.ts
|
|
96243
|
+
var import_child_process5 = require("child_process");
|
|
95850
96244
|
process.env.SPROUTKIT_HEADLESS = "1";
|
|
95851
96245
|
function claudePresent() {
|
|
95852
96246
|
try {
|
|
95853
96247
|
const bin = process.env.SPROUTKIT_CLAUDE_BIN || "claude";
|
|
95854
|
-
return (0,
|
|
96248
|
+
return (0, import_child_process5.spawnSync)(bin, ["--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
|
|
95855
96249
|
} catch {
|
|
95856
96250
|
return false;
|
|
95857
96251
|
}
|
|
@@ -95892,6 +96286,11 @@ async function main() {
|
|
|
95892
96286
|
closeTty();
|
|
95893
96287
|
return;
|
|
95894
96288
|
}
|
|
96289
|
+
if (arg === "doctor") {
|
|
96290
|
+
await runDoctor();
|
|
96291
|
+
closeTty();
|
|
96292
|
+
return;
|
|
96293
|
+
}
|
|
95895
96294
|
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach") {
|
|
95896
96295
|
const subArgs = process.argv.slice(3);
|
|
95897
96296
|
const lic2 = await check();
|
|
@@ -95929,6 +96328,7 @@ async function main() {
|
|
|
95929
96328
|
say(` ${c.bold("sproutkit book <id>")} booking site (--standalone for own-URL)`);
|
|
95930
96329
|
say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
|
|
95931
96330
|
say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
|
|
96331
|
+
say(` ${c.bold("sproutkit doctor")} check everything is wired up`);
|
|
95932
96332
|
say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
|
|
95933
96333
|
say(` ${c.bold("sproutkit sync")} push your leads into the desktop app`);
|
|
95934
96334
|
say(` ${c.bold("sproutkit update")} update to the latest version`);
|
|
@@ -95999,7 +96399,7 @@ async function main() {
|
|
|
95999
96399
|
}
|
|
96000
96400
|
const direct = line.replace(/^\//, "");
|
|
96001
96401
|
const verb = direct.split(/\s+/)[0]?.toLowerCase();
|
|
96002
|
-
const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach"];
|
|
96402
|
+
const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "doctor"];
|
|
96003
96403
|
if (verb === "commands" || verb === "help" || line === "/help" || line === "/?") {
|
|
96004
96404
|
say();
|
|
96005
96405
|
say(c.bold(" Commands (type with or without a leading /):"));
|
|
@@ -96012,7 +96412,7 @@ async function main() {
|
|
|
96012
96412
|
say(` ${c.greenBold("book")} <id> [--standalone] [--archetype slots|reservation|enquiry] [--email \u2026]`);
|
|
96013
96413
|
say(` ${c.dim(" ")} booking on the site, or --standalone for its own URL`);
|
|
96014
96414
|
say(` ${c.greenBold("outreach")} [--ids 3,5] prepare outreach links`);
|
|
96015
|
-
say(c.dim("
|
|
96415
|
+
say(c.dim(" doctor \u2014 check your setup \xB7 /freemode \u2014 open chat \xB7 /taskmode \u2014 precise \xB7 exit"));
|
|
96016
96416
|
say(c.dim(" Or just talk: \u201Cfind cafes in Leeds and build the best two\u201D."));
|
|
96017
96417
|
say();
|
|
96018
96418
|
continue;
|
|
@@ -96029,6 +96429,7 @@ async function main() {
|
|
|
96029
96429
|
else if (verb === "cms") await cmdCms(argv);
|
|
96030
96430
|
else if (verb === "book") await cmdBook(argv);
|
|
96031
96431
|
else if (verb === "outreach") await cmdOutreach(argv);
|
|
96432
|
+
else if (verb === "doctor") await runDoctor();
|
|
96032
96433
|
} catch (e) {
|
|
96033
96434
|
say(c.red(` Error: ${e.message}`));
|
|
96034
96435
|
}
|