sproutkit-code 0.4.0 → 0.5.1
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 +172 -73
- package/dist/skill/SKILL.md +13 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -80617,33 +80617,43 @@ var require_places = __commonJS({
|
|
|
80617
80617
|
return Math.max(0, Math.round(score));
|
|
80618
80618
|
}
|
|
80619
80619
|
var COST_PER_REQUEST_GBP = 0.025;
|
|
80620
|
-
async function
|
|
80621
|
-
const
|
|
80622
|
-
|
|
80623
|
-
|
|
80624
|
-
|
|
80625
|
-
|
|
80626
|
-
|
|
80627
|
-
|
|
80628
|
-
|
|
80620
|
+
async function searchOneType(args) {
|
|
80621
|
+
const body = {
|
|
80622
|
+
textQuery: `${args.type} in ${args.city}`,
|
|
80623
|
+
maxResultCount: 20
|
|
80624
|
+
};
|
|
80625
|
+
if (args.center && args.radiusMeters) {
|
|
80626
|
+
body.locationBias = {
|
|
80627
|
+
circle: {
|
|
80628
|
+
center: { latitude: args.center.lat, longitude: args.center.lng },
|
|
80629
|
+
radius: args.radiusMeters
|
|
80630
|
+
}
|
|
80629
80631
|
};
|
|
80630
|
-
|
|
80631
|
-
|
|
80632
|
-
circle: {
|
|
80633
|
-
center: { latitude: args.center.lat, longitude: args.center.lng },
|
|
80634
|
-
radius: args.radiusMeters
|
|
80635
|
-
}
|
|
80636
|
-
};
|
|
80637
|
-
}
|
|
80632
|
+
}
|
|
80633
|
+
try {
|
|
80638
80634
|
const r = await axios_1.default.post("https://places.googleapis.com/v1/places:searchText", body, {
|
|
80639
80635
|
headers: {
|
|
80640
80636
|
"X-Goog-Api-Key": args.apiKey,
|
|
80641
80637
|
"X-Goog-FieldMask": "places.id,places.displayName,places.formattedAddress,places.nationalPhoneNumber,places.websiteUri,places.rating,places.userRatingCount,places.location,places.priceLevel,places.businessStatus,places.regularOpeningHours"
|
|
80642
80638
|
}
|
|
80643
80639
|
});
|
|
80644
|
-
|
|
80645
|
-
|
|
80646
|
-
|
|
80640
|
+
return { type: args.type, places: r.data.places ?? [] };
|
|
80641
|
+
} catch {
|
|
80642
|
+
return { type: args.type, places: [] };
|
|
80643
|
+
}
|
|
80644
|
+
}
|
|
80645
|
+
async function searchPlaces(args) {
|
|
80646
|
+
const batches = await Promise.all(args.types.map((type) => searchOneType({
|
|
80647
|
+
apiKey: args.apiKey,
|
|
80648
|
+
city: args.city,
|
|
80649
|
+
type,
|
|
80650
|
+
center: args.center,
|
|
80651
|
+
radiusMeters: args.radiusMeters
|
|
80652
|
+
})));
|
|
80653
|
+
const results = [];
|
|
80654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80655
|
+
for (const { type, places } of batches) {
|
|
80656
|
+
for (const p of places) {
|
|
80647
80657
|
if (seen.has(p.id))
|
|
80648
80658
|
continue;
|
|
80649
80659
|
seen.add(p.id);
|
|
@@ -80687,7 +80697,7 @@ var require_places = __commonJS({
|
|
|
80687
80697
|
}
|
|
80688
80698
|
}
|
|
80689
80699
|
results.sort((a, b) => b.score - a.score);
|
|
80690
|
-
return results;
|
|
80700
|
+
return results.slice(0, args.maxResults);
|
|
80691
80701
|
}
|
|
80692
80702
|
function haversineMeters(lat1, lon1, lat2, lon2) {
|
|
80693
80703
|
const R = 6371e3;
|
|
@@ -80729,39 +80739,6 @@ var require_lead_search = __commonJS({
|
|
|
80729
80739
|
exports2.searchAndSaveLeads = searchAndSaveLeads;
|
|
80730
80740
|
var db_1 = require_db();
|
|
80731
80741
|
var places_1 = require_places();
|
|
80732
|
-
var llm_1 = require_llm();
|
|
80733
|
-
async function hasRealWebsite(cand, anthropicKey) {
|
|
80734
|
-
const prompt = `Search the web to determine whether this specific local business has its OWN real, live website (a site they control \u2014 e.g. a .com/.co.uk, Wix, Squarespace, or self-hosted site).
|
|
80735
|
-
|
|
80736
|
-
Business: "${cand.name}"
|
|
80737
|
-
Address: ${cand.address}${cand.city ? `, ${cand.city}` : ""}
|
|
80738
|
-
|
|
80739
|
-
Rules for your judgement:
|
|
80740
|
-
- A Facebook, Instagram, or other social-media page is NOT a website.
|
|
80741
|
-
- A directory listing (Yelp, Yell, TripAdvisor, Google profile, JustEat, Deliveroo, Treatwell, Fresha, etc.) is NOT their own website.
|
|
80742
|
-
- A parked, for-sale, or dead domain with no real content is NOT a live website.
|
|
80743
|
-
- Only answer true if you find a genuine website that clearly belongs to THIS business.
|
|
80744
|
-
- If you are unsure, or can't confirm, answer false.
|
|
80745
|
-
|
|
80746
|
-
Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|null}`;
|
|
80747
|
-
try {
|
|
80748
|
-
const res = await (0, llm_1.generateText)({
|
|
80749
|
-
apiKey: anthropicKey,
|
|
80750
|
-
prompt,
|
|
80751
|
-
allowWebSearch: true,
|
|
80752
|
-
maxTokens: 1024,
|
|
80753
|
-
timeoutMs: 9e4
|
|
80754
|
-
});
|
|
80755
|
-
const raw = res.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
80756
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
80757
|
-
if (!match)
|
|
80758
|
-
return false;
|
|
80759
|
-
const parsed = JSON.parse(match[0]);
|
|
80760
|
-
return parsed.hasWebsite === true;
|
|
80761
|
-
} catch {
|
|
80762
|
-
return false;
|
|
80763
|
-
}
|
|
80764
|
-
}
|
|
80765
80742
|
async function searchAndSaveLeads(args) {
|
|
80766
80743
|
const db2 = (0, db_1.getDb)();
|
|
80767
80744
|
const results = await (0, places_1.searchPlaces)({
|
|
@@ -80773,20 +80750,10 @@ Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|
|
|
|
80773
80750
|
radiusMeters: args.radiusMeters
|
|
80774
80751
|
});
|
|
80775
80752
|
let saved = 0;
|
|
80776
|
-
let verifiedOut = 0;
|
|
80777
80753
|
for (const r of results) {
|
|
80778
80754
|
const before = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
80779
80755
|
if (before.length > 0)
|
|
80780
80756
|
continue;
|
|
80781
|
-
if (args.verifyNoWebsite) {
|
|
80782
|
-
args.onProgress?.(`Checking ${r.name} has no website\u2026`);
|
|
80783
|
-
const real = await hasRealWebsite({ name: r.name, address: r.address, city: args.city }, args.anthropicKey);
|
|
80784
|
-
if (real) {
|
|
80785
|
-
verifiedOut++;
|
|
80786
|
-
args.onProgress?.(`${r.name} already has a website \u2014 skipping.`);
|
|
80787
|
-
continue;
|
|
80788
|
-
}
|
|
80789
|
-
}
|
|
80790
80757
|
db2.run(`INSERT INTO leads
|
|
80791
80758
|
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status)
|
|
80792
80759
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered')`, [
|
|
@@ -80807,7 +80774,7 @@ Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|
|
|
|
80807
80774
|
]);
|
|
80808
80775
|
saved++;
|
|
80809
80776
|
}
|
|
80810
|
-
return { found: results.length, saved
|
|
80777
|
+
return { found: results.length, saved };
|
|
80811
80778
|
}
|
|
80812
80779
|
}
|
|
80813
80780
|
});
|
|
@@ -81146,12 +81113,7 @@ Interested?`;
|
|
|
81146
81113
|
types: categories,
|
|
81147
81114
|
maxResults: Math.min(Number(input.maxResults ?? 25), 100),
|
|
81148
81115
|
center: place ? { lat: place.lat, lng: place.lng } : void 0,
|
|
81149
|
-
radiusMeters: Number(input.radiusMeters ?? 3e3)
|
|
81150
|
-
// Web-search each candidate to confirm no real website exists before
|
|
81151
|
-
// saving. Skippable via verify:false. Fails open on any error.
|
|
81152
|
-
verifyNoWebsite: input.verify !== false,
|
|
81153
|
-
anthropicKey: getDecryptedSetting("anthropic_key"),
|
|
81154
|
-
onProgress: (msg) => ctx.emitProgress?.({ channel: "leads:searching", payload: { msg } })
|
|
81116
|
+
radiusMeters: Number(input.radiusMeters ?? 3e3)
|
|
81155
81117
|
});
|
|
81156
81118
|
ctx.emitProgress?.({
|
|
81157
81119
|
channel: "leads:changed",
|
|
@@ -82320,7 +82282,7 @@ var require_package = __commonJS({
|
|
|
82320
82282
|
"package.json"(exports2, module2) {
|
|
82321
82283
|
module2.exports = {
|
|
82322
82284
|
name: "sproutkit-code",
|
|
82323
|
-
version: "0.
|
|
82285
|
+
version: "0.5.1",
|
|
82324
82286
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
82325
82287
|
bin: {
|
|
82326
82288
|
sproutkit: "./dist/index.js"
|
|
@@ -82576,6 +82538,77 @@ var Status = class {
|
|
|
82576
82538
|
else say(` ${mark} ${label}`);
|
|
82577
82539
|
}
|
|
82578
82540
|
};
|
|
82541
|
+
var STATE_MARK = {
|
|
82542
|
+
queued: () => c.dim("\xB7"),
|
|
82543
|
+
building: () => c.cyan(""),
|
|
82544
|
+
// spinner frame filled in at render time
|
|
82545
|
+
built: () => c.green("\u2713"),
|
|
82546
|
+
failed: () => c.red("\u2717"),
|
|
82547
|
+
skipped: () => c.dim("\u2013")
|
|
82548
|
+
};
|
|
82549
|
+
var StatusBoard = class {
|
|
82550
|
+
rows = [];
|
|
82551
|
+
frame = 0;
|
|
82552
|
+
timer = null;
|
|
82553
|
+
lastLineCount = 0;
|
|
82554
|
+
tty = process.stdout.isTTY && useColor;
|
|
82555
|
+
// Non-TTY: remember the last printed signature per row so we only log changes.
|
|
82556
|
+
lastSig = /* @__PURE__ */ new Map();
|
|
82557
|
+
header = "";
|
|
82558
|
+
start(header, rows) {
|
|
82559
|
+
this.header = header;
|
|
82560
|
+
this.rows = rows;
|
|
82561
|
+
if (!this.tty) {
|
|
82562
|
+
say(c.cyan(` \u27F3 ${header}`));
|
|
82563
|
+
this.rows.forEach((r) => this.logRowIfChanged(r));
|
|
82564
|
+
return;
|
|
82565
|
+
}
|
|
82566
|
+
say(c.cyan(` \u27F3 ${header}`));
|
|
82567
|
+
this.render();
|
|
82568
|
+
this.timer = setInterval(() => {
|
|
82569
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
82570
|
+
this.render();
|
|
82571
|
+
}, 90);
|
|
82572
|
+
}
|
|
82573
|
+
update(row) {
|
|
82574
|
+
const i = this.rows.findIndex((r) => r.key === row.key);
|
|
82575
|
+
if (i >= 0) this.rows[i] = row;
|
|
82576
|
+
else this.rows.push(row);
|
|
82577
|
+
if (!this.tty) this.logRowIfChanged(row);
|
|
82578
|
+
else this.render();
|
|
82579
|
+
}
|
|
82580
|
+
// Replace the whole set at once (e.g. from a job snapshot).
|
|
82581
|
+
set(rows) {
|
|
82582
|
+
this.rows = rows;
|
|
82583
|
+
if (!this.tty) rows.forEach((r) => this.logRowIfChanged(r));
|
|
82584
|
+
else this.render();
|
|
82585
|
+
}
|
|
82586
|
+
line(r) {
|
|
82587
|
+
const mark = r.state === "building" ? c.cyan(FRAMES[this.frame]) : STATE_MARK[r.state]();
|
|
82588
|
+
const detail = r.detail ? c.dim(` \xB7 ${r.detail}`) : "";
|
|
82589
|
+
return ` ${mark} ${r.label}${detail}`;
|
|
82590
|
+
}
|
|
82591
|
+
logRowIfChanged(r) {
|
|
82592
|
+
const sig = `${r.state}:${r.detail ?? ""}`;
|
|
82593
|
+
if (this.lastSig.get(r.key) === sig) return;
|
|
82594
|
+
this.lastSig.set(r.key, sig);
|
|
82595
|
+
const mark = r.state === "built" ? "\u2713" : r.state === "failed" ? "\u2717" : r.state === "skipped" ? "\u2013" : "\u2026";
|
|
82596
|
+
say(` ${mark} ${r.label}${r.detail ? ` \xB7 ${r.detail}` : ""}`);
|
|
82597
|
+
}
|
|
82598
|
+
render() {
|
|
82599
|
+
if (this.lastLineCount > 0) process.stdout.write(`\x1B[${this.lastLineCount}A`);
|
|
82600
|
+
const out = this.rows.map((r) => `\x1B[2K${this.line(r)}`).join("\n");
|
|
82601
|
+
process.stdout.write(out + "\n");
|
|
82602
|
+
this.lastLineCount = this.rows.length;
|
|
82603
|
+
}
|
|
82604
|
+
stop() {
|
|
82605
|
+
if (this.timer) {
|
|
82606
|
+
clearInterval(this.timer);
|
|
82607
|
+
this.timer = null;
|
|
82608
|
+
}
|
|
82609
|
+
if (this.tty) this.render();
|
|
82610
|
+
}
|
|
82611
|
+
};
|
|
82579
82612
|
|
|
82580
82613
|
// src/skill-install.ts
|
|
82581
82614
|
var import_fs = __toESM(require("fs"));
|
|
@@ -83370,6 +83403,70 @@ async function cmdOutreach(argv) {
|
|
|
83370
83403
|
if (result.noPhone) say(c.dim(` ${result.noPhone} had no phone number and were skipped for messaging.`));
|
|
83371
83404
|
say();
|
|
83372
83405
|
}
|
|
83406
|
+
async function cmdBuildAll(argv) {
|
|
83407
|
+
const { flags } = parseArgs(argv);
|
|
83408
|
+
const json = !!flags.json;
|
|
83409
|
+
const ids = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
|
|
83410
|
+
const leadIds = ids.length ? ids : (await runTool("list_leads", { limit: 100 }, makeCtx({ json }))).filter((l) => !l.site_url).map((l) => l.id);
|
|
83411
|
+
if (leadIds.length === 0) {
|
|
83412
|
+
const msg = "No unbuilt leads to build. Find some first, or pass --ids.";
|
|
83413
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.amber(" " + msg));
|
|
83414
|
+
return;
|
|
83415
|
+
}
|
|
83416
|
+
const anthropicKey = getSetting("anthropic_key");
|
|
83417
|
+
const vercelToken = getSetting("vercel_token");
|
|
83418
|
+
if (!vercelToken) {
|
|
83419
|
+
const msg = "Vercel token not set. Run 'sproutkit setup'.";
|
|
83420
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.red(" " + msg));
|
|
83421
|
+
process.exitCode = 1;
|
|
83422
|
+
return;
|
|
83423
|
+
}
|
|
83424
|
+
if (getBackend() === "api" && !anthropicKey) {
|
|
83425
|
+
const msg = "Anthropic API key not set. Run 'sproutkit setup'.";
|
|
83426
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.red(" " + msg));
|
|
83427
|
+
process.exitCode = 1;
|
|
83428
|
+
return;
|
|
83429
|
+
}
|
|
83430
|
+
const concurrency = Math.min(Math.max(Number(flags.concurrency ?? 3), 1), 4);
|
|
83431
|
+
const board = json ? null : new StatusBoard();
|
|
83432
|
+
const stateToRow = (l) => ({
|
|
83433
|
+
key: l.leadId,
|
|
83434
|
+
label: `#${l.leadId} ${l.name}`,
|
|
83435
|
+
state: l.status,
|
|
83436
|
+
detail: l.status === "built" ? l.siteUrl : l.status === "failed" ? l.error : l.message
|
|
83437
|
+
});
|
|
83438
|
+
await new Promise((resolve) => {
|
|
83439
|
+
const emit = (channel, payload) => {
|
|
83440
|
+
const snap2 = payload;
|
|
83441
|
+
if (channel === "auto:progress" && board) board.set(snap2.leads.map(stateToRow));
|
|
83442
|
+
if (channel === "auto:done") {
|
|
83443
|
+
if (board) {
|
|
83444
|
+
board.set(snap2.leads.map(stateToRow));
|
|
83445
|
+
board.stop();
|
|
83446
|
+
}
|
|
83447
|
+
if (json) emitJson(snap2);
|
|
83448
|
+
else {
|
|
83449
|
+
say();
|
|
83450
|
+
say(
|
|
83451
|
+
c.green(` Done \u2014 ${snap2.completed} built`) + (snap2.failed ? c.red(`, ${snap2.failed} failed`) : "") + c.dim(` (concurrency ${snap2.concurrency}).`)
|
|
83452
|
+
);
|
|
83453
|
+
say();
|
|
83454
|
+
}
|
|
83455
|
+
autoSync();
|
|
83456
|
+
resolve();
|
|
83457
|
+
}
|
|
83458
|
+
};
|
|
83459
|
+
const snap = startAutoJob({
|
|
83460
|
+
leadIds,
|
|
83461
|
+
concurrency,
|
|
83462
|
+
anthropicKey: anthropicKey ?? "",
|
|
83463
|
+
vercelToken,
|
|
83464
|
+
placesKey: getSetting("places_key"),
|
|
83465
|
+
emit
|
|
83466
|
+
});
|
|
83467
|
+
if (board) board.start(`Building ${leadIds.length} sites (up to ${concurrency} at once)`, snap.leads.map(stateToRow));
|
|
83468
|
+
});
|
|
83469
|
+
}
|
|
83373
83470
|
|
|
83374
83471
|
// src/index.ts
|
|
83375
83472
|
process.env.SPROUTKIT_HEADLESS = "1";
|
|
@@ -83405,7 +83502,7 @@ async function main() {
|
|
|
83405
83502
|
closeTty();
|
|
83406
83503
|
return;
|
|
83407
83504
|
}
|
|
83408
|
-
if (arg === "find" || arg === "list" || arg === "build" || arg === "outreach") {
|
|
83505
|
+
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "outreach") {
|
|
83409
83506
|
const subArgs = process.argv.slice(3);
|
|
83410
83507
|
const lic2 = await check();
|
|
83411
83508
|
if (lic2.status !== "active") {
|
|
@@ -83419,6 +83516,7 @@ async function main() {
|
|
|
83419
83516
|
if (arg === "find") await cmdFind(subArgs);
|
|
83420
83517
|
else if (arg === "list") await cmdList(subArgs);
|
|
83421
83518
|
else if (arg === "build") await cmdBuild(subArgs);
|
|
83519
|
+
else if (arg === "build-all") await cmdBuildAll(subArgs);
|
|
83422
83520
|
else await cmdOutreach(subArgs);
|
|
83423
83521
|
closeTty();
|
|
83424
83522
|
return;
|
|
@@ -83432,6 +83530,7 @@ async function main() {
|
|
|
83432
83530
|
say(` ${c.bold('sproutkit find "<area>"')} find leads (--categories "barber shop,cafe")`);
|
|
83433
83531
|
say(` ${c.bold("sproutkit list")} list leads (--status, --city, --json)`);
|
|
83434
83532
|
say(` ${c.bold("sproutkit build <id>")} build a website (--instructions, --cms)`);
|
|
83533
|
+
say(` ${c.bold("sproutkit build-all")} build many at once (--ids, --concurrency 3)`);
|
|
83435
83534
|
say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
|
|
83436
83535
|
say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
|
|
83437
83536
|
say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -22,10 +22,8 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
22
22
|
|
|
23
23
|
- **Find leads** — `sproutkit find "<area>" --categories "<type>,<type>" --json`
|
|
24
24
|
- Searches Google Places for businesses of the given categories in the area,
|
|
25
|
-
filters out any that already have a website, and saves
|
|
26
|
-
|
|
27
|
-
saving — so the results are already verified. Social pages and directory
|
|
28
|
-
listings don't count as a website; those businesses are kept as leads.
|
|
25
|
+
filters out any that already have a website (per Google's data), and saves
|
|
26
|
+
the rest as leads.
|
|
29
27
|
- Example: `sproutkit find "Shoreditch, London" --categories "barber shop,cafe" --json`
|
|
30
28
|
- Optional: `--max 25`, `--radius 3000`.
|
|
31
29
|
|
|
@@ -40,6 +38,16 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
40
38
|
- Returns the live `url`. If the lead already has a site, it returns
|
|
41
39
|
`{"ok":false,...}` — tell the user and offer to regenerate.
|
|
42
40
|
|
|
41
|
+
- **Build MANY sites at once** — `sproutkit build-all --json` (optional `--ids 3,5,9`, `--concurrency 3`)
|
|
42
|
+
- Builds several sites in parallel using its own concurrency pool and shows a
|
|
43
|
+
live per-site board. With no `--ids`, it builds every lead that has no site.
|
|
44
|
+
- **Use this for any bulk request** ("build them all", "build the best 5",
|
|
45
|
+
"build sites for leads 1-10"). Do NOT spawn your own subagents to run
|
|
46
|
+
`sproutkit build` many times in parallel — `build-all` owns the
|
|
47
|
+
parallelism, keeps it within the user's subscription rate limits, and
|
|
48
|
+
renders the progress. One `sproutkit build-all` call is correct; N parallel
|
|
49
|
+
`sproutkit build` calls via subagents is not.
|
|
50
|
+
|
|
43
51
|
- **Prepare outreach** — `sproutkit outreach --json` (optional `--ids 3,5,9`)
|
|
44
52
|
- Fills the message template, moves the leads to Contacted, and returns a
|
|
45
53
|
link per lead to open in WhatsApp/SMS. It does NOT auto-send — give the user
|
|
@@ -54,7 +62,7 @@ Chain commands to fulfil what the user asked, one step at a time:
|
|
|
54
62
|
- "Find cafes in Peckham and build the best two" → find, then `sproutkit list --json` to see what came back, then `sproutkit build <id> --json` for the two highest-scoring.
|
|
55
63
|
- "Build a site for The Oak Café" → `sproutkit list --json`, find the matching lead's id, then `sproutkit build <id> --json`.
|
|
56
64
|
- "Message everyone I've built sites for" → `sproutkit outreach --json`, then list the links for the user to open.
|
|
57
|
-
- "Find 20 cafes in Bristol, build them all, and prep outreach" → find,
|
|
65
|
+
- "Find 20 cafes in Bristol, build them all, and prep outreach" → find, then `sproutkit build-all --json` (NOT twenty separate build calls), then `sproutkit outreach --json`.
|
|
58
66
|
|
|
59
67
|
Rules:
|
|
60
68
|
|