sproutkit-code 0.16.0 → 0.18.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 +274 -6
- package/dist/skill/SKILL.md +6 -0
- 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) {
|
|
@@ -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>
|
|
@@ -92918,6 +93089,10 @@ var require_chat_tools = __commonJS({
|
|
|
92918
93089
|
multipage: {
|
|
92919
93090
|
type: "boolean",
|
|
92920
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."
|
|
92921
93096
|
}
|
|
92922
93097
|
},
|
|
92923
93098
|
required: ["id"]
|
|
@@ -93047,6 +93222,7 @@ var require_chat_tools = __commonJS({
|
|
|
93047
93222
|
customInstructions: args.instructions,
|
|
93048
93223
|
includeCms: args.includeCms,
|
|
93049
93224
|
multipage: args.multipage,
|
|
93225
|
+
skipQa: args.skipQa,
|
|
93050
93226
|
blobToken,
|
|
93051
93227
|
onProgress: (msg) => args.ctx.emitProgress?.({
|
|
93052
93228
|
channel: "sites:progress",
|
|
@@ -93339,6 +93515,7 @@ Interested?`;
|
|
|
93339
93515
|
instructions: input.instructions,
|
|
93340
93516
|
includeCms: !!input.includeCms,
|
|
93341
93517
|
multipage: !!input.multipage,
|
|
93518
|
+
skipQa: input.skipQa === true,
|
|
93342
93519
|
ctx
|
|
93343
93520
|
});
|
|
93344
93521
|
return { ...result, id };
|
|
@@ -93356,6 +93533,7 @@ Interested?`;
|
|
|
93356
93533
|
id,
|
|
93357
93534
|
instructions,
|
|
93358
93535
|
multipage: !!input.multipage,
|
|
93536
|
+
skipQa: input.skipQa === true,
|
|
93359
93537
|
ctx
|
|
93360
93538
|
});
|
|
93361
93539
|
return { ...result, id };
|
|
@@ -94419,7 +94597,7 @@ var require_package2 = __commonJS({
|
|
|
94419
94597
|
"package.json"(exports2, module2) {
|
|
94420
94598
|
module2.exports = {
|
|
94421
94599
|
name: "sproutkit-code",
|
|
94422
|
-
version: "0.
|
|
94600
|
+
version: "0.18.0",
|
|
94423
94601
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
94424
94602
|
bin: {
|
|
94425
94603
|
sproutkit: "./dist/index.js"
|
|
@@ -95576,7 +95754,8 @@ async function cmdBuild(argv) {
|
|
|
95576
95754
|
id,
|
|
95577
95755
|
instructions: flags.instructions ? String(flags.instructions) : void 0,
|
|
95578
95756
|
includeCms: !!flags.cms,
|
|
95579
|
-
multipage: !!flags.multipage
|
|
95757
|
+
multipage: !!flags.multipage,
|
|
95758
|
+
skipQa: !!flags["skip-qa"]
|
|
95580
95759
|
},
|
|
95581
95760
|
makeCtx({ json, status })
|
|
95582
95761
|
);
|
|
@@ -95639,6 +95818,90 @@ async function cmdOutreach(argv) {
|
|
|
95639
95818
|
if (result.note) say(c.dim(" " + result.note));
|
|
95640
95819
|
say();
|
|
95641
95820
|
}
|
|
95821
|
+
async function cmdPipeline(argv) {
|
|
95822
|
+
const { flags, rest } = parseArgs(argv);
|
|
95823
|
+
const json = !!flags.json;
|
|
95824
|
+
const xArg = rest.find((r) => /^x\d+$/i.test(r));
|
|
95825
|
+
const concurrency = Math.min(
|
|
95826
|
+
Math.max(Number(flags.concurrency ?? (xArg ? xArg.slice(1) : 2)), 1),
|
|
95827
|
+
4
|
|
95828
|
+
);
|
|
95829
|
+
const withBooking = !!flags.book;
|
|
95830
|
+
const limit = Math.min(Math.max(Number(flags.limit ?? 10), 1), 50);
|
|
95831
|
+
const explicitIds = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
|
|
95832
|
+
const all = await runTool("list_leads", { limit: 100 }, makeCtx({ json }));
|
|
95833
|
+
const targets = (explicitIds.length ? all.filter((l) => explicitIds.includes(l.id)) : all.filter((l) => !l.site_url)).slice(0, limit);
|
|
95834
|
+
if (targets.length === 0) {
|
|
95835
|
+
const msg = "No leads to run. Find some first, or pass --ids.";
|
|
95836
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.amber(" " + msg));
|
|
95837
|
+
return;
|
|
95838
|
+
}
|
|
95839
|
+
const board = json ? null : new StatusBoard();
|
|
95840
|
+
const rowFor = (t, state, detail) => ({ key: t.id, label: `#${t.id} ${t.name}`, state, detail });
|
|
95841
|
+
if (board) {
|
|
95842
|
+
board.start(
|
|
95843
|
+
`Pipeline for ${targets.length} client${targets.length === 1 ? "" : "s"} (${concurrency} at a time)`,
|
|
95844
|
+
targets.map((t) => rowFor(t, "queued"))
|
|
95845
|
+
);
|
|
95846
|
+
}
|
|
95847
|
+
const results = [];
|
|
95848
|
+
const queue = [...targets];
|
|
95849
|
+
async function runOne(t) {
|
|
95850
|
+
const setRow = (state, detail) => board?.update(rowFor(t, state, detail));
|
|
95851
|
+
setRow("building", "building site\u2026");
|
|
95852
|
+
try {
|
|
95853
|
+
const built = await runTool(
|
|
95854
|
+
"build_site",
|
|
95855
|
+
{ id: t.id, skipQa: !!flags["skip-qa"], multipage: !!flags.multipage },
|
|
95856
|
+
makeCtx({ json: true })
|
|
95857
|
+
// suppress per-tool spinners; the board is the UI
|
|
95858
|
+
);
|
|
95859
|
+
if (built && built.ok === false) {
|
|
95860
|
+
setRow("failed", built.error ?? "build failed");
|
|
95861
|
+
results.push({ id: t.id, name: t.name, ok: false, error: built.error });
|
|
95862
|
+
return;
|
|
95863
|
+
}
|
|
95864
|
+
if (withBooking) {
|
|
95865
|
+
setRow("building", "adding booking\u2026");
|
|
95866
|
+
try {
|
|
95867
|
+
await runTool("add_booking_site", { id: t.id }, makeCtx({ json: true }));
|
|
95868
|
+
} catch (e) {
|
|
95869
|
+
setRow("building", `booking skipped (${e.message.slice(0, 40)})`);
|
|
95870
|
+
}
|
|
95871
|
+
}
|
|
95872
|
+
setRow("building", "preparing outreach\u2026");
|
|
95873
|
+
await runTool("prepare_outreach", { ids: [t.id] }, makeCtx({ json: true }));
|
|
95874
|
+
setRow("built", built.url ?? "done");
|
|
95875
|
+
results.push({ id: t.id, name: t.name, ok: true, url: built.url });
|
|
95876
|
+
} catch (e) {
|
|
95877
|
+
setRow("failed", e.message.slice(0, 60));
|
|
95878
|
+
results.push({ id: t.id, name: t.name, ok: false, error: e.message });
|
|
95879
|
+
}
|
|
95880
|
+
}
|
|
95881
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
95882
|
+
for (; ; ) {
|
|
95883
|
+
const next = queue.shift();
|
|
95884
|
+
if (!next) return;
|
|
95885
|
+
await runOne(next);
|
|
95886
|
+
}
|
|
95887
|
+
});
|
|
95888
|
+
await Promise.all(workers);
|
|
95889
|
+
board?.stop();
|
|
95890
|
+
autoSync();
|
|
95891
|
+
const ok = results.filter((r) => r.ok).length;
|
|
95892
|
+
const failed = results.length - ok;
|
|
95893
|
+
if (json) {
|
|
95894
|
+
emitJson({ ok: failed === 0, ran: results.length, succeeded: ok, failed, concurrency, results });
|
|
95895
|
+
if (failed > 0) process.exitCode = 1;
|
|
95896
|
+
return;
|
|
95897
|
+
}
|
|
95898
|
+
say();
|
|
95899
|
+
say(
|
|
95900
|
+
c.green(` Pipeline done \u2014 ${ok} client${ok === 1 ? "" : "s"} built + outreach prepared`) + (failed ? c.red(`, ${failed} failed`) : "") + c.dim(` (concurrency ${concurrency}).`)
|
|
95901
|
+
);
|
|
95902
|
+
say(c.dim(" Open the message links above, or check emailed leads in your Contacted column."));
|
|
95903
|
+
say();
|
|
95904
|
+
}
|
|
95642
95905
|
async function cmdRegenerate(argv) {
|
|
95643
95906
|
const { flags, rest } = parseArgs(argv);
|
|
95644
95907
|
const json = !!flags.json;
|
|
@@ -95661,7 +95924,7 @@ async function cmdRegenerate(argv) {
|
|
|
95661
95924
|
try {
|
|
95662
95925
|
const result = await runTool(
|
|
95663
95926
|
"regenerate_site",
|
|
95664
|
-
{ id, instructions, multipage: !!flags.multipage },
|
|
95927
|
+
{ id, instructions, multipage: !!flags.multipage, skipQa: !!flags["skip-qa"] },
|
|
95665
95928
|
makeCtx({ json, status })
|
|
95666
95929
|
);
|
|
95667
95930
|
if (result && result.ok === false) {
|
|
@@ -95844,6 +96107,7 @@ COMMANDS:
|
|
|
95844
96107
|
- sproutkit cms <id> --json \u2014 add the editable CMS on top of the site we built them
|
|
95845
96108
|
- sproutkit book <id> --json [--standalone] [--instructions "..."] [--archetype slots|reservation|enquiry] [--email x@y.com] \u2014 booking site (--standalone = its own URL for clients who already have a website)
|
|
95846
96109
|
- sproutkit outreach --json [--ids 3,5] \u2014 prepare outreach links
|
|
96110
|
+
- sproutkit pipeline x3 --json [--book] [--limit 10] \u2014 full pipeline (build\u2192booking\u2192outreach) for many clients concurrently
|
|
95847
96111
|
|
|
95848
96112
|
If no command fits the request, say so in one line and name the closest thing you can do. Never invent results.`;
|
|
95849
96113
|
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.`;
|
|
@@ -96112,7 +96376,7 @@ async function main() {
|
|
|
96112
96376
|
closeTty();
|
|
96113
96377
|
return;
|
|
96114
96378
|
}
|
|
96115
|
-
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach") {
|
|
96379
|
+
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach" || arg === "pipeline") {
|
|
96116
96380
|
const subArgs = process.argv.slice(3);
|
|
96117
96381
|
const lic2 = await check();
|
|
96118
96382
|
if (lic2.status !== "active") {
|
|
@@ -96130,6 +96394,7 @@ async function main() {
|
|
|
96130
96394
|
else if (arg === "regenerate") await cmdRegenerate(subArgs);
|
|
96131
96395
|
else if (arg === "cms") await cmdCms(subArgs);
|
|
96132
96396
|
else if (arg === "book") await cmdBook(subArgs);
|
|
96397
|
+
else if (arg === "pipeline") await cmdPipeline(subArgs);
|
|
96133
96398
|
else await cmdOutreach(subArgs);
|
|
96134
96399
|
closeTty();
|
|
96135
96400
|
return;
|
|
@@ -96148,6 +96413,7 @@ async function main() {
|
|
|
96148
96413
|
say(` ${c.bold("sproutkit cms <id>")} add the editable CMS to a built site`);
|
|
96149
96414
|
say(` ${c.bold("sproutkit book <id>")} booking site (--standalone for own-URL)`);
|
|
96150
96415
|
say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
|
|
96416
|
+
say(` ${c.bold("sproutkit pipeline x3")} build+outreach many clients concurrently`);
|
|
96151
96417
|
say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
|
|
96152
96418
|
say(` ${c.bold("sproutkit doctor")} check everything is wired up`);
|
|
96153
96419
|
say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
|
|
@@ -96220,7 +96486,7 @@ async function main() {
|
|
|
96220
96486
|
}
|
|
96221
96487
|
const direct = line.replace(/^\//, "");
|
|
96222
96488
|
const verb = direct.split(/\s+/)[0]?.toLowerCase();
|
|
96223
|
-
const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "doctor"];
|
|
96489
|
+
const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "pipeline", "doctor"];
|
|
96224
96490
|
if (verb === "commands" || verb === "help" || line === "/help" || line === "/?") {
|
|
96225
96491
|
say();
|
|
96226
96492
|
say(c.bold(" Commands (type with or without a leading /):"));
|
|
@@ -96233,6 +96499,7 @@ async function main() {
|
|
|
96233
96499
|
say(` ${c.greenBold("book")} <id> [--standalone] [--archetype slots|reservation|enquiry] [--email \u2026]`);
|
|
96234
96500
|
say(` ${c.dim(" ")} booking on the site, or --standalone for its own URL`);
|
|
96235
96501
|
say(` ${c.greenBold("outreach")} [--ids 3,5] prepare outreach links`);
|
|
96502
|
+
say(` ${c.greenBold("pipeline")} x3 [--book] [--limit 10] build + outreach many, concurrently`);
|
|
96236
96503
|
say(c.dim(" doctor \u2014 check your setup \xB7 /freemode \u2014 open chat \xB7 /taskmode \u2014 precise \xB7 exit"));
|
|
96237
96504
|
say(c.dim(" Or just talk: \u201Cfind cafes in Leeds and build the best two\u201D."));
|
|
96238
96505
|
say();
|
|
@@ -96250,6 +96517,7 @@ async function main() {
|
|
|
96250
96517
|
else if (verb === "cms") await cmdCms(argv);
|
|
96251
96518
|
else if (verb === "book") await cmdBook(argv);
|
|
96252
96519
|
else if (verb === "outreach") await cmdOutreach(argv);
|
|
96520
|
+
else if (verb === "pipeline") await cmdPipeline(argv);
|
|
96253
96521
|
else if (verb === "doctor") await runDoctor();
|
|
96254
96522
|
} catch (e) {
|
|
96255
96523
|
say(c.red(` Error: ${e.message}`));
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -66,6 +66,12 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
66
66
|
- Use `book --standalone` when the user says the client "already has a
|
|
67
67
|
website" or "just needs a booking page".
|
|
68
68
|
|
|
69
|
+
- **Run the full pipeline for many clients** — `sproutkit pipeline x3 --json` (optional `--book`, `--limit 10`, `--ids 3,5`)
|
|
70
|
+
- Builds → (optionally adds booking) → prepares outreach for several clients
|
|
71
|
+
CONCURRENTLY (capped at 4), refilling as each finishes. Use for "run the
|
|
72
|
+
pipeline", "do the whole thing for my leads", "build and message them all".
|
|
73
|
+
- Run `sproutkit find` FIRST — pipeline works through leads you already have.
|
|
74
|
+
|
|
69
75
|
- **Prepare outreach** — `sproutkit outreach --json` (optional `--ids 3,5,9`)
|
|
70
76
|
- Per lead: if it has a PHONE, returns a WhatsApp/SMS link for the user to
|
|
71
77
|
open (not auto-sent); if it has NO phone but an EMAIL (and SMTP is set up),
|