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 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.16.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}`));
@@ -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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutkit-code",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Sproutkit in your terminal — find leads, build sites, run outreach, all via chat.",
5
5
  "bin": {
6
6
  "sproutkit": "./dist/index.js"