sproutkit-code 0.4.0 → 0.5.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 +141 -52
- package/dist/skill/SKILL.md +13 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -80729,39 +80729,6 @@ var require_lead_search = __commonJS({
|
|
|
80729
80729
|
exports2.searchAndSaveLeads = searchAndSaveLeads;
|
|
80730
80730
|
var db_1 = require_db();
|
|
80731
80731
|
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
80732
|
async function searchAndSaveLeads(args) {
|
|
80766
80733
|
const db2 = (0, db_1.getDb)();
|
|
80767
80734
|
const results = await (0, places_1.searchPlaces)({
|
|
@@ -80773,20 +80740,10 @@ Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|
|
|
|
80773
80740
|
radiusMeters: args.radiusMeters
|
|
80774
80741
|
});
|
|
80775
80742
|
let saved = 0;
|
|
80776
|
-
let verifiedOut = 0;
|
|
80777
80743
|
for (const r of results) {
|
|
80778
80744
|
const before = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
80779
80745
|
if (before.length > 0)
|
|
80780
80746
|
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
80747
|
db2.run(`INSERT INTO leads
|
|
80791
80748
|
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status)
|
|
80792
80749
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered')`, [
|
|
@@ -80807,7 +80764,7 @@ Respond with ONLY compact JSON, no prose: {"hasWebsite": boolean, "url": string|
|
|
|
80807
80764
|
]);
|
|
80808
80765
|
saved++;
|
|
80809
80766
|
}
|
|
80810
|
-
return { found: results.length, saved
|
|
80767
|
+
return { found: results.length, saved };
|
|
80811
80768
|
}
|
|
80812
80769
|
}
|
|
80813
80770
|
});
|
|
@@ -81146,12 +81103,7 @@ Interested?`;
|
|
|
81146
81103
|
types: categories,
|
|
81147
81104
|
maxResults: Math.min(Number(input.maxResults ?? 25), 100),
|
|
81148
81105
|
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 } })
|
|
81106
|
+
radiusMeters: Number(input.radiusMeters ?? 3e3)
|
|
81155
81107
|
});
|
|
81156
81108
|
ctx.emitProgress?.({
|
|
81157
81109
|
channel: "leads:changed",
|
|
@@ -82320,7 +82272,7 @@ var require_package = __commonJS({
|
|
|
82320
82272
|
"package.json"(exports2, module2) {
|
|
82321
82273
|
module2.exports = {
|
|
82322
82274
|
name: "sproutkit-code",
|
|
82323
|
-
version: "0.
|
|
82275
|
+
version: "0.5.0",
|
|
82324
82276
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
82325
82277
|
bin: {
|
|
82326
82278
|
sproutkit: "./dist/index.js"
|
|
@@ -82576,6 +82528,77 @@ var Status = class {
|
|
|
82576
82528
|
else say(` ${mark} ${label}`);
|
|
82577
82529
|
}
|
|
82578
82530
|
};
|
|
82531
|
+
var STATE_MARK = {
|
|
82532
|
+
queued: () => c.dim("\xB7"),
|
|
82533
|
+
building: () => c.cyan(""),
|
|
82534
|
+
// spinner frame filled in at render time
|
|
82535
|
+
built: () => c.green("\u2713"),
|
|
82536
|
+
failed: () => c.red("\u2717"),
|
|
82537
|
+
skipped: () => c.dim("\u2013")
|
|
82538
|
+
};
|
|
82539
|
+
var StatusBoard = class {
|
|
82540
|
+
rows = [];
|
|
82541
|
+
frame = 0;
|
|
82542
|
+
timer = null;
|
|
82543
|
+
lastLineCount = 0;
|
|
82544
|
+
tty = process.stdout.isTTY && useColor;
|
|
82545
|
+
// Non-TTY: remember the last printed signature per row so we only log changes.
|
|
82546
|
+
lastSig = /* @__PURE__ */ new Map();
|
|
82547
|
+
header = "";
|
|
82548
|
+
start(header, rows) {
|
|
82549
|
+
this.header = header;
|
|
82550
|
+
this.rows = rows;
|
|
82551
|
+
if (!this.tty) {
|
|
82552
|
+
say(c.cyan(` \u27F3 ${header}`));
|
|
82553
|
+
this.rows.forEach((r) => this.logRowIfChanged(r));
|
|
82554
|
+
return;
|
|
82555
|
+
}
|
|
82556
|
+
say(c.cyan(` \u27F3 ${header}`));
|
|
82557
|
+
this.render();
|
|
82558
|
+
this.timer = setInterval(() => {
|
|
82559
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
82560
|
+
this.render();
|
|
82561
|
+
}, 90);
|
|
82562
|
+
}
|
|
82563
|
+
update(row) {
|
|
82564
|
+
const i = this.rows.findIndex((r) => r.key === row.key);
|
|
82565
|
+
if (i >= 0) this.rows[i] = row;
|
|
82566
|
+
else this.rows.push(row);
|
|
82567
|
+
if (!this.tty) this.logRowIfChanged(row);
|
|
82568
|
+
else this.render();
|
|
82569
|
+
}
|
|
82570
|
+
// Replace the whole set at once (e.g. from a job snapshot).
|
|
82571
|
+
set(rows) {
|
|
82572
|
+
this.rows = rows;
|
|
82573
|
+
if (!this.tty) rows.forEach((r) => this.logRowIfChanged(r));
|
|
82574
|
+
else this.render();
|
|
82575
|
+
}
|
|
82576
|
+
line(r) {
|
|
82577
|
+
const mark = r.state === "building" ? c.cyan(FRAMES[this.frame]) : STATE_MARK[r.state]();
|
|
82578
|
+
const detail = r.detail ? c.dim(` \xB7 ${r.detail}`) : "";
|
|
82579
|
+
return ` ${mark} ${r.label}${detail}`;
|
|
82580
|
+
}
|
|
82581
|
+
logRowIfChanged(r) {
|
|
82582
|
+
const sig = `${r.state}:${r.detail ?? ""}`;
|
|
82583
|
+
if (this.lastSig.get(r.key) === sig) return;
|
|
82584
|
+
this.lastSig.set(r.key, sig);
|
|
82585
|
+
const mark = r.state === "built" ? "\u2713" : r.state === "failed" ? "\u2717" : r.state === "skipped" ? "\u2013" : "\u2026";
|
|
82586
|
+
say(` ${mark} ${r.label}${r.detail ? ` \xB7 ${r.detail}` : ""}`);
|
|
82587
|
+
}
|
|
82588
|
+
render() {
|
|
82589
|
+
if (this.lastLineCount > 0) process.stdout.write(`\x1B[${this.lastLineCount}A`);
|
|
82590
|
+
const out = this.rows.map((r) => `\x1B[2K${this.line(r)}`).join("\n");
|
|
82591
|
+
process.stdout.write(out + "\n");
|
|
82592
|
+
this.lastLineCount = this.rows.length;
|
|
82593
|
+
}
|
|
82594
|
+
stop() {
|
|
82595
|
+
if (this.timer) {
|
|
82596
|
+
clearInterval(this.timer);
|
|
82597
|
+
this.timer = null;
|
|
82598
|
+
}
|
|
82599
|
+
if (this.tty) this.render();
|
|
82600
|
+
}
|
|
82601
|
+
};
|
|
82579
82602
|
|
|
82580
82603
|
// src/skill-install.ts
|
|
82581
82604
|
var import_fs = __toESM(require("fs"));
|
|
@@ -83370,6 +83393,70 @@ async function cmdOutreach(argv) {
|
|
|
83370
83393
|
if (result.noPhone) say(c.dim(` ${result.noPhone} had no phone number and were skipped for messaging.`));
|
|
83371
83394
|
say();
|
|
83372
83395
|
}
|
|
83396
|
+
async function cmdBuildAll(argv) {
|
|
83397
|
+
const { flags } = parseArgs(argv);
|
|
83398
|
+
const json = !!flags.json;
|
|
83399
|
+
const ids = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
|
|
83400
|
+
const leadIds = ids.length ? ids : (await runTool("list_leads", { limit: 100 }, makeCtx({ json }))).filter((l) => !l.site_url).map((l) => l.id);
|
|
83401
|
+
if (leadIds.length === 0) {
|
|
83402
|
+
const msg = "No unbuilt leads to build. Find some first, or pass --ids.";
|
|
83403
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.amber(" " + msg));
|
|
83404
|
+
return;
|
|
83405
|
+
}
|
|
83406
|
+
const anthropicKey = getSetting("anthropic_key");
|
|
83407
|
+
const vercelToken = getSetting("vercel_token");
|
|
83408
|
+
if (!vercelToken) {
|
|
83409
|
+
const msg = "Vercel token not set. Run 'sproutkit setup'.";
|
|
83410
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.red(" " + msg));
|
|
83411
|
+
process.exitCode = 1;
|
|
83412
|
+
return;
|
|
83413
|
+
}
|
|
83414
|
+
if (getBackend() === "api" && !anthropicKey) {
|
|
83415
|
+
const msg = "Anthropic API key not set. Run 'sproutkit setup'.";
|
|
83416
|
+
json ? emitJson({ ok: false, error: msg }) : say(c.red(" " + msg));
|
|
83417
|
+
process.exitCode = 1;
|
|
83418
|
+
return;
|
|
83419
|
+
}
|
|
83420
|
+
const concurrency = Math.min(Math.max(Number(flags.concurrency ?? 3), 1), 4);
|
|
83421
|
+
const board = json ? null : new StatusBoard();
|
|
83422
|
+
const stateToRow = (l) => ({
|
|
83423
|
+
key: l.leadId,
|
|
83424
|
+
label: `#${l.leadId} ${l.name}`,
|
|
83425
|
+
state: l.status,
|
|
83426
|
+
detail: l.status === "built" ? l.siteUrl : l.status === "failed" ? l.error : l.message
|
|
83427
|
+
});
|
|
83428
|
+
await new Promise((resolve) => {
|
|
83429
|
+
const emit = (channel, payload) => {
|
|
83430
|
+
const snap2 = payload;
|
|
83431
|
+
if (channel === "auto:progress" && board) board.set(snap2.leads.map(stateToRow));
|
|
83432
|
+
if (channel === "auto:done") {
|
|
83433
|
+
if (board) {
|
|
83434
|
+
board.set(snap2.leads.map(stateToRow));
|
|
83435
|
+
board.stop();
|
|
83436
|
+
}
|
|
83437
|
+
if (json) emitJson(snap2);
|
|
83438
|
+
else {
|
|
83439
|
+
say();
|
|
83440
|
+
say(
|
|
83441
|
+
c.green(` Done \u2014 ${snap2.completed} built`) + (snap2.failed ? c.red(`, ${snap2.failed} failed`) : "") + c.dim(` (concurrency ${snap2.concurrency}).`)
|
|
83442
|
+
);
|
|
83443
|
+
say();
|
|
83444
|
+
}
|
|
83445
|
+
autoSync();
|
|
83446
|
+
resolve();
|
|
83447
|
+
}
|
|
83448
|
+
};
|
|
83449
|
+
const snap = startAutoJob({
|
|
83450
|
+
leadIds,
|
|
83451
|
+
concurrency,
|
|
83452
|
+
anthropicKey: anthropicKey ?? "",
|
|
83453
|
+
vercelToken,
|
|
83454
|
+
placesKey: getSetting("places_key"),
|
|
83455
|
+
emit
|
|
83456
|
+
});
|
|
83457
|
+
if (board) board.start(`Building ${leadIds.length} sites (up to ${concurrency} at once)`, snap.leads.map(stateToRow));
|
|
83458
|
+
});
|
|
83459
|
+
}
|
|
83373
83460
|
|
|
83374
83461
|
// src/index.ts
|
|
83375
83462
|
process.env.SPROUTKIT_HEADLESS = "1";
|
|
@@ -83405,7 +83492,7 @@ async function main() {
|
|
|
83405
83492
|
closeTty();
|
|
83406
83493
|
return;
|
|
83407
83494
|
}
|
|
83408
|
-
if (arg === "find" || arg === "list" || arg === "build" || arg === "outreach") {
|
|
83495
|
+
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "outreach") {
|
|
83409
83496
|
const subArgs = process.argv.slice(3);
|
|
83410
83497
|
const lic2 = await check();
|
|
83411
83498
|
if (lic2.status !== "active") {
|
|
@@ -83419,6 +83506,7 @@ async function main() {
|
|
|
83419
83506
|
if (arg === "find") await cmdFind(subArgs);
|
|
83420
83507
|
else if (arg === "list") await cmdList(subArgs);
|
|
83421
83508
|
else if (arg === "build") await cmdBuild(subArgs);
|
|
83509
|
+
else if (arg === "build-all") await cmdBuildAll(subArgs);
|
|
83422
83510
|
else await cmdOutreach(subArgs);
|
|
83423
83511
|
closeTty();
|
|
83424
83512
|
return;
|
|
@@ -83432,6 +83520,7 @@ async function main() {
|
|
|
83432
83520
|
say(` ${c.bold('sproutkit find "<area>"')} find leads (--categories "barber shop,cafe")`);
|
|
83433
83521
|
say(` ${c.bold("sproutkit list")} list leads (--status, --city, --json)`);
|
|
83434
83522
|
say(` ${c.bold("sproutkit build <id>")} build a website (--instructions, --cms)`);
|
|
83523
|
+
say(` ${c.bold("sproutkit build-all")} build many at once (--ids, --concurrency 3)`);
|
|
83435
83524
|
say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
|
|
83436
83525
|
say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
|
|
83437
83526
|
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
|
|