sproutkit-code 0.17.0 → 0.19.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
@@ -93841,7 +93841,7 @@ var require_auto_mode = __commonJS({
93841
93841
  exports2.startAutoJob = startAutoJob2;
93842
93842
  exports2.pauseAutoJob = pauseAutoJob;
93843
93843
  exports2.resumeAutoJob = resumeAutoJob;
93844
- exports2.cancelAutoJob = cancelAutoJob;
93844
+ exports2.cancelAutoJob = cancelAutoJob2;
93845
93845
  exports2.getAutoJobStatus = getAutoJobStatus;
93846
93846
  var db_1 = require_db();
93847
93847
  var builder_1 = require_builder();
@@ -94045,7 +94045,7 @@ var require_auto_mode = __commonJS({
94045
94045
  activeJob.resume();
94046
94046
  return activeJob.snapshot();
94047
94047
  }
94048
- function cancelAutoJob() {
94048
+ function cancelAutoJob2() {
94049
94049
  if (!activeJob)
94050
94050
  return null;
94051
94051
  activeJob.cancel();
@@ -94597,7 +94597,7 @@ var require_package2 = __commonJS({
94597
94597
  "package.json"(exports2, module2) {
94598
94598
  module2.exports = {
94599
94599
  name: "sproutkit-code",
94600
- version: "0.17.0",
94600
+ version: "0.19.0",
94601
94601
  description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
94602
94602
  bin: {
94603
94603
  sproutkit: "./dist/index.js"
@@ -94652,6 +94652,7 @@ var getDb = db.getDb;
94652
94652
  var chatWithTools = chat.chatWithTools;
94653
94653
  var runTool = chatTools.runTool;
94654
94654
  var startAutoJob = autoMode.startAutoJob;
94655
+ var cancelAutoJob = autoMode.cancelAutoJob;
94655
94656
  var provisionCmsForLead = cms.provisionCmsForLead;
94656
94657
  var generatePreset = bookingPreset.generatePreset;
94657
94658
  var deployBookingOnly = builder.deployBookingOnly;
@@ -94814,6 +94815,40 @@ function closeTty() {
94814
94815
  rl?.close();
94815
94816
  rl = null;
94816
94817
  }
94818
+ function onStopRequested(onStop) {
94819
+ let stopped = false;
94820
+ const stdin = process.stdin;
94821
+ const wasRaw = !!stdin.isTTY && stdin.isRaw;
94822
+ const handler = (buf) => {
94823
+ const s = buf.toString();
94824
+ if (s === "q" || s === "Q" || s === "\x1B" || s === "") {
94825
+ if (!stopped) {
94826
+ stopped = true;
94827
+ onStop?.();
94828
+ }
94829
+ }
94830
+ };
94831
+ if (stdin.isTTY) {
94832
+ try {
94833
+ stdin.setRawMode(true);
94834
+ stdin.resume();
94835
+ stdin.on("data", handler);
94836
+ } catch {
94837
+ }
94838
+ }
94839
+ return {
94840
+ stopped: () => stopped,
94841
+ release: () => {
94842
+ if (!stdin.isTTY) return;
94843
+ try {
94844
+ stdin.removeListener("data", handler);
94845
+ stdin.setRawMode(wasRaw);
94846
+ stdin.pause();
94847
+ } catch {
94848
+ }
94849
+ }
94850
+ };
94851
+ }
94817
94852
  function say(msg = "") {
94818
94853
  process.stdout.write(msg + "\n");
94819
94854
  }
@@ -95818,6 +95853,103 @@ async function cmdOutreach(argv) {
95818
95853
  if (result.note) say(c.dim(" " + result.note));
95819
95854
  say();
95820
95855
  }
95856
+ async function cmdPipeline(argv) {
95857
+ const { flags, rest } = parseArgs(argv);
95858
+ const json = !!flags.json;
95859
+ const xArg = rest.find((r) => /^x\d+$/i.test(r));
95860
+ const concurrency = Math.min(
95861
+ Math.max(Number(flags.concurrency ?? (xArg ? xArg.slice(1) : 2)), 1),
95862
+ 4
95863
+ );
95864
+ const withBooking = !!flags.book;
95865
+ const limit = Math.min(Math.max(Number(flags.limit ?? 10), 1), 50);
95866
+ const explicitIds = String(flags.ids ?? "").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && n > 0);
95867
+ const all = await runTool("list_leads", { limit: 100 }, makeCtx({ json }));
95868
+ const targets = (explicitIds.length ? all.filter((l) => explicitIds.includes(l.id)) : all.filter((l) => !l.site_url)).slice(0, limit);
95869
+ if (targets.length === 0) {
95870
+ const msg = "No leads to run. Find some first, or pass --ids.";
95871
+ json ? emitJson({ ok: false, error: msg }) : say(c.amber(" " + msg));
95872
+ return;
95873
+ }
95874
+ const board = json ? null : new StatusBoard();
95875
+ const rowFor = (t, state, detail) => ({ key: t.id, label: `#${t.id} ${t.name}`, state, detail });
95876
+ if (board) {
95877
+ board.start(
95878
+ `Pipeline for ${targets.length} client${targets.length === 1 ? "" : "s"} (${concurrency} at a time)`,
95879
+ targets.map((t) => rowFor(t, "queued"))
95880
+ );
95881
+ say(c.dim(" Press q to stop after the current clients finish."));
95882
+ }
95883
+ const stop = json ? null : onStopRequested(() => {
95884
+ say(c.amber("\n Stopping \u2014 finishing current clients, not starting new ones\u2026"));
95885
+ });
95886
+ let cancelled = 0;
95887
+ const results = [];
95888
+ const queue = [...targets];
95889
+ async function runOne(t) {
95890
+ const setRow = (state, detail) => board?.update(rowFor(t, state, detail));
95891
+ setRow("building", "building site\u2026");
95892
+ try {
95893
+ const built = await runTool(
95894
+ "build_site",
95895
+ { id: t.id, skipQa: !!flags["skip-qa"], multipage: !!flags.multipage },
95896
+ makeCtx({ json: true })
95897
+ // suppress per-tool spinners; the board is the UI
95898
+ );
95899
+ if (built && built.ok === false) {
95900
+ setRow("failed", built.error ?? "build failed");
95901
+ results.push({ id: t.id, name: t.name, ok: false, error: built.error });
95902
+ return;
95903
+ }
95904
+ if (withBooking) {
95905
+ setRow("building", "adding booking\u2026");
95906
+ try {
95907
+ await runTool("add_booking_site", { id: t.id }, makeCtx({ json: true }));
95908
+ } catch (e) {
95909
+ setRow("building", `booking skipped (${e.message.slice(0, 40)})`);
95910
+ }
95911
+ }
95912
+ setRow("building", "preparing outreach\u2026");
95913
+ await runTool("prepare_outreach", { ids: [t.id] }, makeCtx({ json: true }));
95914
+ setRow("built", built.url ?? "done");
95915
+ results.push({ id: t.id, name: t.name, ok: true, url: built.url });
95916
+ } catch (e) {
95917
+ setRow("failed", e.message.slice(0, 60));
95918
+ results.push({ id: t.id, name: t.name, ok: false, error: e.message });
95919
+ }
95920
+ }
95921
+ const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
95922
+ for (; ; ) {
95923
+ if (stop?.stopped()) {
95924
+ for (const t of queue.splice(0)) {
95925
+ board?.update(rowFor(t, "skipped", "stopped"));
95926
+ cancelled++;
95927
+ }
95928
+ return;
95929
+ }
95930
+ const next = queue.shift();
95931
+ if (!next) return;
95932
+ await runOne(next);
95933
+ }
95934
+ });
95935
+ await Promise.all(workers);
95936
+ stop?.release();
95937
+ board?.stop();
95938
+ autoSync();
95939
+ const ok = results.filter((r) => r.ok).length;
95940
+ const failed = results.length - ok;
95941
+ if (json) {
95942
+ emitJson({ ok: failed === 0, ran: results.length, succeeded: ok, failed, concurrency, results });
95943
+ if (failed > 0) process.exitCode = 1;
95944
+ return;
95945
+ }
95946
+ say();
95947
+ say(
95948
+ c.green(` Pipeline ${cancelled ? "stopped" : "done"} \u2014 ${ok} client${ok === 1 ? "" : "s"} built + outreach prepared`) + (failed ? c.red(`, ${failed} failed`) : "") + (cancelled ? c.amber(`, ${cancelled} not started`) : "") + c.dim(` (concurrency ${concurrency}).`)
95949
+ );
95950
+ say(c.dim(" Open the message links above, or check emailed leads in your Contacted column."));
95951
+ say();
95952
+ }
95821
95953
  async function cmdRegenerate(argv) {
95822
95954
  const { flags, rest } = parseArgs(argv);
95823
95955
  const json = !!flags.json;
@@ -95949,6 +96081,10 @@ async function cmdBuildAll(argv) {
95949
96081
  }
95950
96082
  const concurrency = Math.min(Math.max(Number(flags.concurrency ?? 3), 1), 4);
95951
96083
  const board = json ? null : new StatusBoard();
96084
+ const stop = json ? null : onStopRequested(() => {
96085
+ say(c.amber("\n Stopping \u2014 finishing in-flight builds, not starting new ones\u2026"));
96086
+ cancelAutoJob();
96087
+ });
95952
96088
  const stateToRow = (l) => ({
95953
96089
  key: l.leadId,
95954
96090
  label: `#${l.leadId} ${l.name}`,
@@ -95964,6 +96100,7 @@ async function cmdBuildAll(argv) {
95964
96100
  board.set(snap2.leads.map(stateToRow));
95965
96101
  board.stop();
95966
96102
  }
96103
+ stop?.release();
95967
96104
  if (json) emitJson(snap2);
95968
96105
  else {
95969
96106
  say();
@@ -95984,7 +96121,10 @@ async function cmdBuildAll(argv) {
95984
96121
  placesKey: getSetting("places_key"),
95985
96122
  emit
95986
96123
  });
95987
- if (board) board.start(`Building ${leadIds.length} sites (up to ${concurrency} at once)`, snap.leads.map(stateToRow));
96124
+ if (board) {
96125
+ board.start(`Building ${leadIds.length} sites (up to ${concurrency} at once)`, snap.leads.map(stateToRow));
96126
+ say(c.dim(" Press q to stop after the current builds finish."));
96127
+ }
95988
96128
  });
95989
96129
  }
95990
96130
 
@@ -96023,6 +96163,7 @@ COMMANDS:
96023
96163
  - sproutkit cms <id> --json \u2014 add the editable CMS on top of the site we built them
96024
96164
  - 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)
96025
96165
  - sproutkit outreach --json [--ids 3,5] \u2014 prepare outreach links
96166
+ - sproutkit pipeline x3 --json [--book] [--limit 10] \u2014 full pipeline (build\u2192booking\u2192outreach) for many clients concurrently
96026
96167
 
96027
96168
  If no command fits the request, say so in one line and name the closest thing you can do. Never invent results.`;
96028
96169
  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.`;
@@ -96291,7 +96432,7 @@ async function main() {
96291
96432
  closeTty();
96292
96433
  return;
96293
96434
  }
96294
- if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach") {
96435
+ if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "outreach" || arg === "pipeline") {
96295
96436
  const subArgs = process.argv.slice(3);
96296
96437
  const lic2 = await check();
96297
96438
  if (lic2.status !== "active") {
@@ -96309,6 +96450,7 @@ async function main() {
96309
96450
  else if (arg === "regenerate") await cmdRegenerate(subArgs);
96310
96451
  else if (arg === "cms") await cmdCms(subArgs);
96311
96452
  else if (arg === "book") await cmdBook(subArgs);
96453
+ else if (arg === "pipeline") await cmdPipeline(subArgs);
96312
96454
  else await cmdOutreach(subArgs);
96313
96455
  closeTty();
96314
96456
  return;
@@ -96327,6 +96469,7 @@ async function main() {
96327
96469
  say(` ${c.bold("sproutkit cms <id>")} add the editable CMS to a built site`);
96328
96470
  say(` ${c.bold("sproutkit book <id>")} booking site (--standalone for own-URL)`);
96329
96471
  say(` ${c.bold("sproutkit outreach")} prep outreach links (--ids 3,5)`);
96472
+ say(` ${c.bold("sproutkit pipeline x3")} build+outreach many clients concurrently`);
96330
96473
  say(` ${c.bold("sproutkit setup")} set or update keys, licence, backend`);
96331
96474
  say(` ${c.bold("sproutkit doctor")} check everything is wired up`);
96332
96475
  say(` ${c.bold("sproutkit models")} list Claude models and pick one`);
@@ -96399,7 +96542,7 @@ async function main() {
96399
96542
  }
96400
96543
  const direct = line.replace(/^\//, "");
96401
96544
  const verb = direct.split(/\s+/)[0]?.toLowerCase();
96402
- const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "doctor"];
96545
+ const DIRECT_VERBS = ["find", "list", "build", "build-all", "regenerate", "cms", "book", "outreach", "pipeline", "doctor"];
96403
96546
  if (verb === "commands" || verb === "help" || line === "/help" || line === "/?") {
96404
96547
  say();
96405
96548
  say(c.bold(" Commands (type with or without a leading /):"));
@@ -96412,6 +96555,7 @@ async function main() {
96412
96555
  say(` ${c.greenBold("book")} <id> [--standalone] [--archetype slots|reservation|enquiry] [--email \u2026]`);
96413
96556
  say(` ${c.dim(" ")} booking on the site, or --standalone for its own URL`);
96414
96557
  say(` ${c.greenBold("outreach")} [--ids 3,5] prepare outreach links`);
96558
+ say(` ${c.greenBold("pipeline")} x3 [--book] [--limit 10] build + outreach many, concurrently`);
96415
96559
  say(c.dim(" doctor \u2014 check your setup \xB7 /freemode \u2014 open chat \xB7 /taskmode \u2014 precise \xB7 exit"));
96416
96560
  say(c.dim(" Or just talk: \u201Cfind cafes in Leeds and build the best two\u201D."));
96417
96561
  say();
@@ -96429,6 +96573,7 @@ async function main() {
96429
96573
  else if (verb === "cms") await cmdCms(argv);
96430
96574
  else if (verb === "book") await cmdBook(argv);
96431
96575
  else if (verb === "outreach") await cmdOutreach(argv);
96576
+ else if (verb === "pipeline") await cmdPipeline(argv);
96432
96577
  else if (verb === "doctor") await runDoctor();
96433
96578
  } catch (e) {
96434
96579
  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.17.0",
3
+ "version": "0.19.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"