sproutkit-code 0.10.0 → 0.12.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 +142 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -80731,29 +80731,39 @@ var require_places = __commonJS({
|
|
|
80731
80731
|
}
|
|
80732
80732
|
var COST_PER_REQUEST_GBP = 0.025;
|
|
80733
80733
|
async function searchOneType(args) {
|
|
80734
|
-
const
|
|
80735
|
-
|
|
80736
|
-
|
|
80737
|
-
|
|
80738
|
-
|
|
80739
|
-
|
|
80740
|
-
circle: {
|
|
80741
|
-
center: { latitude: args.center.lat, longitude: args.center.lng },
|
|
80742
|
-
radius: args.radiusMeters
|
|
80743
|
-
}
|
|
80734
|
+
const all = [];
|
|
80735
|
+
let pageToken;
|
|
80736
|
+
for (let page = 0; page < 3; page++) {
|
|
80737
|
+
const body = {
|
|
80738
|
+
textQuery: `${args.type} in ${args.city}`,
|
|
80739
|
+
maxResultCount: 20
|
|
80744
80740
|
};
|
|
80741
|
+
if (args.center && args.radiusMeters) {
|
|
80742
|
+
body.locationBias = {
|
|
80743
|
+
circle: {
|
|
80744
|
+
center: { latitude: args.center.lat, longitude: args.center.lng },
|
|
80745
|
+
radius: args.radiusMeters
|
|
80746
|
+
}
|
|
80747
|
+
};
|
|
80748
|
+
}
|
|
80749
|
+
if (pageToken)
|
|
80750
|
+
body.pageToken = pageToken;
|
|
80751
|
+
try {
|
|
80752
|
+
const r = await axios_1.default.post("https://places.googleapis.com/v1/places:searchText", body, {
|
|
80753
|
+
headers: {
|
|
80754
|
+
"X-Goog-Api-Key": args.apiKey,
|
|
80755
|
+
"X-Goog-FieldMask": "nextPageToken,places.id,places.displayName,places.formattedAddress,places.nationalPhoneNumber,places.websiteUri,places.rating,places.userRatingCount,places.location,places.priceLevel,places.businessStatus,places.regularOpeningHours"
|
|
80756
|
+
}
|
|
80757
|
+
});
|
|
80758
|
+
all.push(...r.data.places ?? []);
|
|
80759
|
+
pageToken = r.data.nextPageToken;
|
|
80760
|
+
if (!pageToken)
|
|
80761
|
+
break;
|
|
80762
|
+
} catch {
|
|
80763
|
+
break;
|
|
80764
|
+
}
|
|
80745
80765
|
}
|
|
80746
|
-
|
|
80747
|
-
const r = await axios_1.default.post("https://places.googleapis.com/v1/places:searchText", body, {
|
|
80748
|
-
headers: {
|
|
80749
|
-
"X-Goog-Api-Key": args.apiKey,
|
|
80750
|
-
"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"
|
|
80751
|
-
}
|
|
80752
|
-
});
|
|
80753
|
-
return { type: args.type, places: r.data.places ?? [] };
|
|
80754
|
-
} catch {
|
|
80755
|
-
return { type: args.type, places: [] };
|
|
80756
|
-
}
|
|
80766
|
+
return { type: args.type, places: all };
|
|
80757
80767
|
}
|
|
80758
80768
|
function processRawPlaces(type, places, center, radiusMeters) {
|
|
80759
80769
|
const out = [];
|
|
@@ -81022,7 +81032,7 @@ var require_chat_tools = __commonJS({
|
|
|
81022
81032
|
},
|
|
81023
81033
|
radiusMeters: {
|
|
81024
81034
|
type: "number",
|
|
81025
|
-
description: "Search radius around the location in metres (default
|
|
81035
|
+
description: "Search radius around the location in metres (default 10000 = 10km). Use a larger radius for big cities where central businesses mostly have websites; smaller (e.g. 3000) for a tight local search."
|
|
81026
81036
|
}
|
|
81027
81037
|
},
|
|
81028
81038
|
required: ["location", "categories"]
|
|
@@ -81327,7 +81337,11 @@ Interested?`;
|
|
|
81327
81337
|
types: categories,
|
|
81328
81338
|
maxResults: Math.min(Number(input.maxResults ?? 25), 100),
|
|
81329
81339
|
center: place ? { lat: place.lat, lng: place.lng } : void 0,
|
|
81330
|
-
|
|
81340
|
+
// Default widened from 3km → 10km. A 3km circle in a website-
|
|
81341
|
+
// saturated city centre (e.g. central London) yields almost no
|
|
81342
|
+
// no-website leads; 10km covers a realistic catchment. Caller/model
|
|
81343
|
+
// can still override for a tighter or wider search.
|
|
81344
|
+
radiusMeters: Number(input.radiusMeters ?? 1e4)
|
|
81331
81345
|
},
|
|
81332
81346
|
// Emit each lead the instant it's saved, so the UI/CLI streams results
|
|
81333
81347
|
// in rather than waiting for every category to finish.
|
|
@@ -82505,12 +82519,53 @@ Return this exact shape:
|
|
|
82505
82519
|
}
|
|
82506
82520
|
});
|
|
82507
82521
|
|
|
82522
|
+
// ../desktop/dist/main/lib/vercel-store.js
|
|
82523
|
+
var require_vercel_store = __commonJS({
|
|
82524
|
+
"../desktop/dist/main/lib/vercel-store.js"(exports2) {
|
|
82525
|
+
"use strict";
|
|
82526
|
+
var __importDefault = exports2 && exports2.__importDefault || function(mod) {
|
|
82527
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
82528
|
+
};
|
|
82529
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
82530
|
+
exports2.createBlobStore = createBlobStore2;
|
|
82531
|
+
var axios_1 = __importDefault(require_axios());
|
|
82532
|
+
async function createBlobStore2(args) {
|
|
82533
|
+
const name = (args.name || "sproutkit").slice(0, 70);
|
|
82534
|
+
const url = "https://api.vercel.com/storage/stores/blob" + (args.teamId ? `?teamId=${encodeURIComponent(args.teamId)}` : "");
|
|
82535
|
+
try {
|
|
82536
|
+
const res = await axios_1.default.post(url, { name }, {
|
|
82537
|
+
headers: {
|
|
82538
|
+
Authorization: `Bearer ${args.vercelToken}`,
|
|
82539
|
+
"Content-Type": "application/json"
|
|
82540
|
+
},
|
|
82541
|
+
timeout: 3e4
|
|
82542
|
+
});
|
|
82543
|
+
const store = res.data?.store ?? {};
|
|
82544
|
+
const storeId = store.id || store.storeId;
|
|
82545
|
+
return {
|
|
82546
|
+
ok: true,
|
|
82547
|
+
storeId,
|
|
82548
|
+
dashboardUrl: "https://vercel.com/dashboard/stores"
|
|
82549
|
+
};
|
|
82550
|
+
} catch (e) {
|
|
82551
|
+
const err = e;
|
|
82552
|
+
const status = err.response?.status;
|
|
82553
|
+
const msg = err.response?.data?.error?.message ?? err.message;
|
|
82554
|
+
if (status === 409 || /already|exists|conflict/i.test(msg)) {
|
|
82555
|
+
return { ok: false, alreadyExists: true, error: msg };
|
|
82556
|
+
}
|
|
82557
|
+
return { ok: false, error: msg };
|
|
82558
|
+
}
|
|
82559
|
+
}
|
|
82560
|
+
}
|
|
82561
|
+
});
|
|
82562
|
+
|
|
82508
82563
|
// package.json
|
|
82509
82564
|
var require_package = __commonJS({
|
|
82510
82565
|
"package.json"(exports2, module2) {
|
|
82511
82566
|
module2.exports = {
|
|
82512
82567
|
name: "sproutkit-code",
|
|
82513
|
-
version: "0.
|
|
82568
|
+
version: "0.12.0",
|
|
82514
82569
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
82515
82570
|
bin: {
|
|
82516
82571
|
sproutkit: "./dist/index.js"
|
|
@@ -82557,6 +82612,7 @@ var autoMode = require_auto_mode();
|
|
|
82557
82612
|
var cms = require_cms_provision();
|
|
82558
82613
|
var bookingPreset = require_booking_preset();
|
|
82559
82614
|
var builder = require_builder();
|
|
82615
|
+
var vercelStore = require_vercel_store();
|
|
82560
82616
|
var models = require_models();
|
|
82561
82617
|
var llm = require_llm();
|
|
82562
82618
|
var initDatabase = db.initDatabase;
|
|
@@ -82568,6 +82624,7 @@ var provisionCmsForLead = cms.provisionCmsForLead;
|
|
|
82568
82624
|
var generatePreset = bookingPreset.generatePreset;
|
|
82569
82625
|
var deployBookingOnly = builder.deployBookingOnly;
|
|
82570
82626
|
var deployStandaloneBooking = builder.deployStandaloneBooking;
|
|
82627
|
+
var createBlobStore = vercelStore.createBlobStore;
|
|
82571
82628
|
var refreshModels = models.refreshModels;
|
|
82572
82629
|
var refreshIfStale = models.refreshIfStale;
|
|
82573
82630
|
var listCachedModels = models.listCachedModels;
|
|
@@ -83029,6 +83086,25 @@ async function runOnboarding(force = false) {
|
|
|
83029
83086
|
}
|
|
83030
83087
|
|
|
83031
83088
|
// src/invoke-ipc.ts
|
|
83089
|
+
async function requireBlobToken(vercelToken, emit, leadId) {
|
|
83090
|
+
const existing = getSetting("blob_token");
|
|
83091
|
+
if (existing) return existing;
|
|
83092
|
+
emit("bookings:progress", { leadId, msg: "No Blob token yet \u2014 creating a Blob store for you\u2026" });
|
|
83093
|
+
const created = await createBlobStore({ vercelToken, name: "sproutkit" });
|
|
83094
|
+
if (created.ok) {
|
|
83095
|
+
throw new Error(
|
|
83096
|
+
"Created a Vercel Blob store for you \u2713 \u2014 one last one-time step: open " + (created.dashboardUrl || "https://vercel.com/dashboard/stores") + " , open the 'sproutkit' store, copy its BLOB_READ_WRITE_TOKEN, then run 'sproutkit setup' and paste it. You only do this once \u2014 it's reused for every booking site."
|
|
83097
|
+
);
|
|
83098
|
+
}
|
|
83099
|
+
if (created.alreadyExists) {
|
|
83100
|
+
throw new Error(
|
|
83101
|
+
"You already have a Blob store. Open https://vercel.com/dashboard/stores , copy its BLOB_READ_WRITE_TOKEN, and run 'sproutkit setup' to paste it (one-time \u2014 reused for all sites)."
|
|
83102
|
+
);
|
|
83103
|
+
}
|
|
83104
|
+
throw new Error(
|
|
83105
|
+
`Booking needs a Vercel Blob token and I couldn't auto-create a store (${created.error ?? "unknown"}). Create one at https://vercel.com/dashboard/stores (Create \u2192 Blob), copy BLOB_READ_WRITE_TOKEN, then 'sproutkit setup'.`
|
|
83106
|
+
);
|
|
83107
|
+
}
|
|
83032
83108
|
function makeInvokeIpc(emit) {
|
|
83033
83109
|
return async (channel, payload) => {
|
|
83034
83110
|
if (channel === "auto:build:start") {
|
|
@@ -83080,11 +83156,10 @@ function makeInvokeIpc(emit) {
|
|
|
83080
83156
|
if (!lead) throw new Error("Lead not found");
|
|
83081
83157
|
const anthropicKey = getSetting("anthropic_key");
|
|
83082
83158
|
const vercelToken = getSetting("vercel_token");
|
|
83083
|
-
const blobToken = getSetting("blob_token");
|
|
83084
83159
|
const resendKey = getSetting("resend_key");
|
|
83085
83160
|
if (!anthropicKey) throw new Error("Anthropic API key not set");
|
|
83086
83161
|
if (!vercelToken) throw new Error("Vercel token not set");
|
|
83087
|
-
|
|
83162
|
+
const blobToken = await requireBlobToken(vercelToken, emit, p.leadId);
|
|
83088
83163
|
emit("bookings:progress", { leadId: p.leadId, msg: "Designing the booking form\u2026" });
|
|
83089
83164
|
const leadInfo = lead;
|
|
83090
83165
|
const config = await generatePreset({
|
|
@@ -83122,10 +83197,9 @@ function makeInvokeIpc(emit) {
|
|
|
83122
83197
|
const lead = db2.get("SELECT * FROM leads WHERE id = ?", [p.leadId]);
|
|
83123
83198
|
if (!lead) throw new Error("Lead not found");
|
|
83124
83199
|
const vercelToken = getSetting("vercel_token");
|
|
83125
|
-
const blobToken = getSetting("blob_token");
|
|
83126
83200
|
const resendKey = getSetting("resend_key");
|
|
83127
83201
|
if (!vercelToken) throw new Error("Vercel token not set");
|
|
83128
|
-
|
|
83202
|
+
const blobToken = await requireBlobToken(vercelToken, emit, p.leadId);
|
|
83129
83203
|
emit("bookings:progress", { leadId: p.leadId, msg: "Designing the booking form\u2026" });
|
|
83130
83204
|
const leadInfo = lead;
|
|
83131
83205
|
const config = await generatePreset({
|
|
@@ -83802,8 +83876,14 @@ var TASK_SYSTEM_PROMPT = `You operate the "sproutkit" CLI on the user's behalf.
|
|
|
83802
83876
|
HOW TO ACT \u2014 this is strict:
|
|
83803
83877
|
- Fulfil each request by running the matching sproutkit command(s) via Bash. Nothing else.
|
|
83804
83878
|
- Do NOT read, list, or explore the repo. Do NOT narrate a plan, think out loud, or look for a skill. No preamble, no "let me\u2026". Either ask a crisp question (below) or run the command.
|
|
83805
|
-
- Always pass --json and parse it.
|
|
83806
|
-
|
|
83879
|
+
- Always pass --json and parse it. Get lead ids from \`sproutkit list --json\` \u2014 never guess an id.
|
|
83880
|
+
|
|
83881
|
+
OUTPUT FORMAT \u2014 terse, scannable, NO editorialising:
|
|
83882
|
+
- Never write rambling sentences or commentary about the results ("well short of 20, so the source is returning few matches" \u2014 DON'T). Just present the data.
|
|
83883
|
+
- For find: one line "Found N (M new)", then one line per NEW lead: "#<id> <name> \xB7 <type>".
|
|
83884
|
+
- For list: one line per lead "#<id> <name> \xB7 <type> \xB7 <city> \xB7 <status>" (cap at ~15, then "\u2026and K more").
|
|
83885
|
+
- For build/book: one line "\u2713 <name> \u2014 <url>".
|
|
83886
|
+
- Use a plain "#<id> <name>" style. No tables, no fluff, no "Here's what I found". If a result count is low, state the number plainly \u2014 do NOT speculate about why unless asked.
|
|
83807
83887
|
|
|
83808
83888
|
GATHER REQUIRED CHOICES FIRST \u2014 ask, don't guess, don't chatter:
|
|
83809
83889
|
When a request is missing information the command needs, ask a SHORT, STRUCTURED question that lists the options \u2014 then wait. Present choices as a tight numbered/bulleted list, no rambling. Specifically:
|
|
@@ -84063,6 +84143,38 @@ async function main() {
|
|
|
84063
84143
|
say();
|
|
84064
84144
|
continue;
|
|
84065
84145
|
}
|
|
84146
|
+
const direct = line.replace(/^\//, "");
|
|
84147
|
+
const verb = direct.split(/\s+/)[0]?.toLowerCase();
|
|
84148
|
+
const DIRECT_VERBS = ["find", "list", "build", "build-all", "book", "outreach"];
|
|
84149
|
+
if (verb === "commands" || verb === "help" || line === "/help" || line === "/?") {
|
|
84150
|
+
say();
|
|
84151
|
+
say(c.bold(" Commands (type with or without a leading /):"));
|
|
84152
|
+
say(` ${c.greenBold("find")} "<area>" --categories "cafe,barber" find leads`);
|
|
84153
|
+
say(` ${c.greenBold("list")} [--status <s>] [--city <c>] list leads`);
|
|
84154
|
+
say(` ${c.greenBold("build")} <id> [--instructions "\u2026"] [--cms] build a demo site`);
|
|
84155
|
+
say(` ${c.greenBold("build-all")} [--ids 3,5] [--concurrency 3] build many at once`);
|
|
84156
|
+
say(` ${c.greenBold("book")} <id> [--standalone] [--archetype \u2026] booking site`);
|
|
84157
|
+
say(` ${c.greenBold("outreach")} [--ids 3,5] prepare outreach links`);
|
|
84158
|
+
say(c.dim(" /freemode \u2014 open conversation \xB7 /taskmode \u2014 precise \xB7 exit \u2014 quit"));
|
|
84159
|
+
say(c.dim(" Or just talk: \u201Cfind cafes in Leeds and build the best two\u201D."));
|
|
84160
|
+
say();
|
|
84161
|
+
continue;
|
|
84162
|
+
}
|
|
84163
|
+
const isSlash = line.startsWith("/");
|
|
84164
|
+
if (DIRECT_VERBS.includes(verb) && (isSlash || !freeMode)) {
|
|
84165
|
+
const argv = (direct.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).slice(1).map((a) => a.replace(/^['"]|['"]$/g, ""));
|
|
84166
|
+
try {
|
|
84167
|
+
if (verb === "find") await cmdFind(argv);
|
|
84168
|
+
else if (verb === "list") await cmdList(argv);
|
|
84169
|
+
else if (verb === "build") await cmdBuild(argv);
|
|
84170
|
+
else if (verb === "build-all") await cmdBuildAll(argv);
|
|
84171
|
+
else if (verb === "book") await cmdBook(argv);
|
|
84172
|
+
else if (verb === "outreach") await cmdOutreach(argv);
|
|
84173
|
+
} catch (e) {
|
|
84174
|
+
say(c.red(` Error: ${e.message}`));
|
|
84175
|
+
}
|
|
84176
|
+
continue;
|
|
84177
|
+
}
|
|
84066
84178
|
try {
|
|
84067
84179
|
if (bridged) {
|
|
84068
84180
|
const r = await runClaudeTurn({
|