sproutkit-code 1.7.0 → 1.8.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 +667 -8
- package/dist/skill/SKILL.md +11 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -229,6 +229,12 @@ var require_db = __commonJS({
|
|
|
229
229
|
// WITHOUT the client's password. Means handing the client full ownership
|
|
230
230
|
// doesn't cost the agency its reporting.
|
|
231
231
|
"report_key TEXT",
|
|
232
|
+
// `sproutkit audit` findings: how bad the business's EXISTING site is, and
|
|
233
|
+
// the specific problems to lead outreach with. Distinct from lead_kind's
|
|
234
|
+
// absence-based gaps.
|
|
235
|
+
"site_score INTEGER",
|
|
236
|
+
"site_issues TEXT",
|
|
237
|
+
"audited_at TEXT",
|
|
232
238
|
// Reply loop: when we last contacted them, and the latest outcome. Kept on
|
|
233
239
|
// the lead (as well as contact_log) so `followups` is a single cheap query.
|
|
234
240
|
"last_contacted_at TEXT",
|
|
@@ -82790,6 +82796,389 @@ var require_booking_detect = __commonJS({
|
|
|
82790
82796
|
}
|
|
82791
82797
|
});
|
|
82792
82798
|
|
|
82799
|
+
// ../desktop/dist/main/lib/site-audit.js
|
|
82800
|
+
var require_site_audit = __commonJS({
|
|
82801
|
+
"../desktop/dist/main/lib/site-audit.js"(exports2) {
|
|
82802
|
+
"use strict";
|
|
82803
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
82804
|
+
if (k2 === void 0) k2 = k;
|
|
82805
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
82806
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
82807
|
+
desc = { enumerable: true, get: function() {
|
|
82808
|
+
return m[k];
|
|
82809
|
+
} };
|
|
82810
|
+
}
|
|
82811
|
+
Object.defineProperty(o, k2, desc);
|
|
82812
|
+
}) : (function(o, m, k, k2) {
|
|
82813
|
+
if (k2 === void 0) k2 = k;
|
|
82814
|
+
o[k2] = m[k];
|
|
82815
|
+
}));
|
|
82816
|
+
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
82817
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
82818
|
+
}) : function(o, v) {
|
|
82819
|
+
o["default"] = v;
|
|
82820
|
+
});
|
|
82821
|
+
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
82822
|
+
var ownKeys = function(o) {
|
|
82823
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
82824
|
+
var ar = [];
|
|
82825
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
82826
|
+
return ar;
|
|
82827
|
+
};
|
|
82828
|
+
return ownKeys(o);
|
|
82829
|
+
};
|
|
82830
|
+
return function(mod) {
|
|
82831
|
+
if (mod && mod.__esModule) return mod;
|
|
82832
|
+
var result = {};
|
|
82833
|
+
if (mod != null) {
|
|
82834
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
82835
|
+
}
|
|
82836
|
+
__setModuleDefault(result, mod);
|
|
82837
|
+
return result;
|
|
82838
|
+
};
|
|
82839
|
+
})();
|
|
82840
|
+
var __importDefault = exports2 && exports2.__importDefault || function(mod) {
|
|
82841
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
82842
|
+
};
|
|
82843
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
82844
|
+
exports2.auditSite = auditSite;
|
|
82845
|
+
exports2.isRebuildProspect = isRebuildProspect;
|
|
82846
|
+
exports2.headlineIssues = headlineIssues;
|
|
82847
|
+
var axios_1 = __importDefault(require_axios());
|
|
82848
|
+
var tls = __importStar(require("tls"));
|
|
82849
|
+
var url_1 = require("url");
|
|
82850
|
+
var WEIGHT = {
|
|
82851
|
+
critical: 30,
|
|
82852
|
+
major: 15,
|
|
82853
|
+
minor: 6
|
|
82854
|
+
};
|
|
82855
|
+
async function checkTls(hostname) {
|
|
82856
|
+
return new Promise((resolve) => {
|
|
82857
|
+
let settled = false;
|
|
82858
|
+
const done = (r) => {
|
|
82859
|
+
if (settled)
|
|
82860
|
+
return;
|
|
82861
|
+
settled = true;
|
|
82862
|
+
socket.destroy();
|
|
82863
|
+
resolve(r);
|
|
82864
|
+
};
|
|
82865
|
+
const socket = tls.connect({ host: hostname, port: 443, servername: hostname, timeout: 12e3 }, () => {
|
|
82866
|
+
if (!socket.authorized) {
|
|
82867
|
+
done({
|
|
82868
|
+
ok: false,
|
|
82869
|
+
issue: {
|
|
82870
|
+
code: "tls_invalid",
|
|
82871
|
+
label: "The security certificate isn\u2019t valid \u2014 browsers warn visitors before they can see the site",
|
|
82872
|
+
severity: "critical",
|
|
82873
|
+
detail: socket.authorizationError ? String(socket.authorizationError) : void 0
|
|
82874
|
+
}
|
|
82875
|
+
});
|
|
82876
|
+
return;
|
|
82877
|
+
}
|
|
82878
|
+
const cert = socket.getPeerCertificate();
|
|
82879
|
+
if (!cert || !cert.valid_to) {
|
|
82880
|
+
done({ ok: true });
|
|
82881
|
+
return;
|
|
82882
|
+
}
|
|
82883
|
+
const days = Math.round((new Date(cert.valid_to).getTime() - Date.now()) / 864e5);
|
|
82884
|
+
if (days < 0) {
|
|
82885
|
+
done({
|
|
82886
|
+
ok: false,
|
|
82887
|
+
daysToExpiry: days,
|
|
82888
|
+
issue: {
|
|
82889
|
+
code: "tls_expired",
|
|
82890
|
+
label: "The security certificate has expired \u2014 visitors see a full-page warning",
|
|
82891
|
+
severity: "critical",
|
|
82892
|
+
detail: `expired ${Math.abs(days)} days ago`
|
|
82893
|
+
}
|
|
82894
|
+
});
|
|
82895
|
+
return;
|
|
82896
|
+
}
|
|
82897
|
+
if (days <= 14) {
|
|
82898
|
+
done({
|
|
82899
|
+
ok: true,
|
|
82900
|
+
daysToExpiry: days,
|
|
82901
|
+
issue: {
|
|
82902
|
+
code: "tls_expiring",
|
|
82903
|
+
label: "The security certificate expires within two weeks",
|
|
82904
|
+
severity: "major",
|
|
82905
|
+
detail: `${days} days left`
|
|
82906
|
+
}
|
|
82907
|
+
});
|
|
82908
|
+
return;
|
|
82909
|
+
}
|
|
82910
|
+
done({ ok: true, daysToExpiry: days });
|
|
82911
|
+
});
|
|
82912
|
+
socket.on("error", (err) => {
|
|
82913
|
+
const msg = err.message || "";
|
|
82914
|
+
const code = err.code ?? "";
|
|
82915
|
+
let issue;
|
|
82916
|
+
if (/certificate has expired|CERT_HAS_EXPIRED/i.test(msg + code)) {
|
|
82917
|
+
issue = {
|
|
82918
|
+
code: "tls_expired",
|
|
82919
|
+
label: "The security certificate has expired \u2014 visitors see a full-page browser warning before they reach the site",
|
|
82920
|
+
severity: "critical",
|
|
82921
|
+
detail: msg
|
|
82922
|
+
};
|
|
82923
|
+
} else if (/self.signed|SELF_SIGNED/i.test(msg + code)) {
|
|
82924
|
+
issue = {
|
|
82925
|
+
code: "tls_self_signed",
|
|
82926
|
+
label: "The security certificate isn\u2019t from a recognised authority, so browsers warn visitors away",
|
|
82927
|
+
severity: "critical",
|
|
82928
|
+
detail: msg
|
|
82929
|
+
};
|
|
82930
|
+
} else if (/altnames|hostname\/ip does not match|ERR_TLS_CERT_ALTNAME/i.test(msg + code)) {
|
|
82931
|
+
issue = {
|
|
82932
|
+
code: "tls_wrong_host",
|
|
82933
|
+
label: "The security certificate was issued for a different domain, so browsers reject it",
|
|
82934
|
+
severity: "critical",
|
|
82935
|
+
detail: msg
|
|
82936
|
+
};
|
|
82937
|
+
} else if (/ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ECONNRESET/i.test(msg + code)) {
|
|
82938
|
+
issue = {
|
|
82939
|
+
code: "tls_unavailable",
|
|
82940
|
+
label: "The site has no working HTTPS \u2014 Chrome labels it \u201CNot secure\u201D to every visitor",
|
|
82941
|
+
severity: "critical",
|
|
82942
|
+
detail: msg
|
|
82943
|
+
};
|
|
82944
|
+
} else {
|
|
82945
|
+
issue = {
|
|
82946
|
+
code: "tls_error",
|
|
82947
|
+
label: "Secure connections to the site fail",
|
|
82948
|
+
severity: "major",
|
|
82949
|
+
detail: msg
|
|
82950
|
+
};
|
|
82951
|
+
}
|
|
82952
|
+
done({ ok: false, issue });
|
|
82953
|
+
});
|
|
82954
|
+
socket.on("timeout", () => {
|
|
82955
|
+
done({
|
|
82956
|
+
ok: false,
|
|
82957
|
+
issue: {
|
|
82958
|
+
code: "tls_timeout",
|
|
82959
|
+
label: "Secure connections to the site time out",
|
|
82960
|
+
severity: "major"
|
|
82961
|
+
}
|
|
82962
|
+
});
|
|
82963
|
+
});
|
|
82964
|
+
});
|
|
82965
|
+
}
|
|
82966
|
+
function markupIssues(html, finalUrl) {
|
|
82967
|
+
const issues = [];
|
|
82968
|
+
const head = html.slice(0, 6e4);
|
|
82969
|
+
if (!/<meta[^>]+name=["']?viewport/i.test(head)) {
|
|
82970
|
+
issues.push({
|
|
82971
|
+
code: "not_mobile",
|
|
82972
|
+
label: "Not built for phones \u2014 the page loads zoomed out and has to be pinched to read",
|
|
82973
|
+
severity: "critical"
|
|
82974
|
+
});
|
|
82975
|
+
}
|
|
82976
|
+
const tableCount = (html.match(/<table/gi) ?? []).length;
|
|
82977
|
+
if (tableCount >= 3 || /<table[^>]*>[\s\S]{0,400}<table/i.test(html)) {
|
|
82978
|
+
issues.push({
|
|
82979
|
+
code: "table_layout",
|
|
82980
|
+
label: "Built with table layouts, a technique abandoned around 2010",
|
|
82981
|
+
severity: "major",
|
|
82982
|
+
detail: `${tableCount} tables`
|
|
82983
|
+
});
|
|
82984
|
+
}
|
|
82985
|
+
if (/<(font|center|marquee|blink)\b/i.test(html)) {
|
|
82986
|
+
const tag = html.match(/<(font|center|marquee|blink)\b/i)?.[1]?.toLowerCase();
|
|
82987
|
+
issues.push({
|
|
82988
|
+
code: "obsolete_tags",
|
|
82989
|
+
label: `Uses HTML tags (<${tag}>) that have been removed from the web standard`,
|
|
82990
|
+
severity: "major"
|
|
82991
|
+
});
|
|
82992
|
+
}
|
|
82993
|
+
if (/\.swf\b|application\/x-shockwave-flash|<embed[^>]+flash/i.test(html)) {
|
|
82994
|
+
issues.push({
|
|
82995
|
+
code: "flash",
|
|
82996
|
+
label: "Contains Flash content, which no browser has supported since 2020 \u2014 that part of the site is invisible",
|
|
82997
|
+
severity: "critical"
|
|
82998
|
+
});
|
|
82999
|
+
}
|
|
83000
|
+
const jq = html.match(/jquery[.-]?(\d+)\.(\d+)(?:\.(\d+))?(?:\.min)?\.js/i);
|
|
83001
|
+
if (jq) {
|
|
83002
|
+
const major = Number(jq[1]);
|
|
83003
|
+
if (major < 3) {
|
|
83004
|
+
issues.push({
|
|
83005
|
+
code: "old_jquery",
|
|
83006
|
+
label: "Runs a jQuery version with publicly known security vulnerabilities",
|
|
83007
|
+
severity: "major",
|
|
83008
|
+
detail: `jQuery ${jq[1]}.${jq[2]}${jq[3] ? "." + jq[3] : ""}`
|
|
83009
|
+
});
|
|
83010
|
+
}
|
|
83011
|
+
}
|
|
83012
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
83013
|
+
const years = [...html.matchAll(/(?:©|©|copyright)\s*(?:\d{4}\s*[-–]\s*)?(\d{4})/gi)].map((m) => Number(m[1])).filter((y) => y >= 1995 && y <= year);
|
|
83014
|
+
if (years.length) {
|
|
83015
|
+
const newest = Math.max(...years);
|
|
83016
|
+
const age = year - newest;
|
|
83017
|
+
if (age >= 2) {
|
|
83018
|
+
issues.push({
|
|
83019
|
+
code: "stale_copyright",
|
|
83020
|
+
label: `The footer still says ${newest} \u2014 visitors read that as \u201Cpossibly closed down\u201D`,
|
|
83021
|
+
severity: age >= 5 ? "major" : "minor",
|
|
83022
|
+
detail: `${age} years out of date`
|
|
83023
|
+
});
|
|
83024
|
+
}
|
|
83025
|
+
}
|
|
83026
|
+
if (/under construction|coming soon|website is being (?:built|updated)/i.test(head)) {
|
|
83027
|
+
issues.push({
|
|
83028
|
+
code: "under_construction",
|
|
83029
|
+
label: "Still shows an \u201Cunder construction\u201D placeholder",
|
|
83030
|
+
severity: "critical"
|
|
83031
|
+
});
|
|
83032
|
+
}
|
|
83033
|
+
if (/this domain (?:is|may be) for sale|parked (?:free )?(?:at|by)|godaddy\.com\/domainfind|default web page/i.test(head)) {
|
|
83034
|
+
issues.push({
|
|
83035
|
+
code: "parked",
|
|
83036
|
+
label: "The domain shows a parking page rather than a real website",
|
|
83037
|
+
severity: "critical"
|
|
83038
|
+
});
|
|
83039
|
+
}
|
|
83040
|
+
if (finalUrl.startsWith("https://")) {
|
|
83041
|
+
const insecure = html.match(/(?:src|href)=["']http:\/\/(?!localhost)/gi);
|
|
83042
|
+
if (insecure && insecure.length >= 2) {
|
|
83043
|
+
issues.push({
|
|
83044
|
+
code: "mixed_content",
|
|
83045
|
+
label: "Loads images or scripts insecurely, so browsers block them and parts of the page break",
|
|
83046
|
+
severity: "major",
|
|
83047
|
+
detail: `${insecure.length} insecure references`
|
|
83048
|
+
});
|
|
83049
|
+
}
|
|
83050
|
+
}
|
|
83051
|
+
const title = html.match(/<title[^>]*>([\s\S]{0,200}?)<\/title>/i)?.[1]?.trim();
|
|
83052
|
+
if (!title) {
|
|
83053
|
+
issues.push({
|
|
83054
|
+
code: "no_title",
|
|
83055
|
+
label: "The page has no title, so search results show a bare web address",
|
|
83056
|
+
severity: "major"
|
|
83057
|
+
});
|
|
83058
|
+
} else if (/^(home|untitled|new page \d*|document|index|website)$/i.test(title)) {
|
|
83059
|
+
issues.push({
|
|
83060
|
+
code: "default_title",
|
|
83061
|
+
label: `The page title is still the template default (\u201C${title}\u201D), which is what Google shows`,
|
|
83062
|
+
severity: "major"
|
|
83063
|
+
});
|
|
83064
|
+
}
|
|
83065
|
+
if (!/<meta[^>]+name=["']?description/i.test(head)) {
|
|
83066
|
+
issues.push({
|
|
83067
|
+
code: "no_description",
|
|
83068
|
+
label: "No search description, so Google invents one from whatever text it finds",
|
|
83069
|
+
severity: "minor"
|
|
83070
|
+
});
|
|
83071
|
+
}
|
|
83072
|
+
if (/<frameset|<frame\b/i.test(html)) {
|
|
83073
|
+
issues.push({
|
|
83074
|
+
code: "frames",
|
|
83075
|
+
label: "Built with frames, which search engines cannot index properly",
|
|
83076
|
+
severity: "critical"
|
|
83077
|
+
});
|
|
83078
|
+
}
|
|
83079
|
+
return issues;
|
|
83080
|
+
}
|
|
83081
|
+
async function auditSite(websiteUrl) {
|
|
83082
|
+
const raw = (websiteUrl ?? "").trim();
|
|
83083
|
+
if (!raw)
|
|
83084
|
+
return { url: raw, reachable: false, score: 0, issues: [], error: "No URL" };
|
|
83085
|
+
const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
83086
|
+
let hostname;
|
|
83087
|
+
try {
|
|
83088
|
+
hostname = new url_1.URL(withScheme).hostname;
|
|
83089
|
+
} catch {
|
|
83090
|
+
return { url: raw, reachable: false, score: 0, issues: [], error: "Unparseable URL" };
|
|
83091
|
+
}
|
|
83092
|
+
const issues = [];
|
|
83093
|
+
const listedAsHttp = /^http:\/\//i.test(raw);
|
|
83094
|
+
const [tlsResult, page] = await Promise.all([
|
|
83095
|
+
checkTls(hostname).catch(() => ({ ok: true })),
|
|
83096
|
+
fetchPage(withScheme)
|
|
83097
|
+
]);
|
|
83098
|
+
if (tlsResult.issue)
|
|
83099
|
+
issues.push(tlsResult.issue);
|
|
83100
|
+
if (listedAsHttp && !tlsResult.issue) {
|
|
83101
|
+
issues.push({
|
|
83102
|
+
code: "listed_insecure",
|
|
83103
|
+
label: "The address the business gives out starts with http:// \u2014 visitors following it get a \u201CNot secure\u201D warning",
|
|
83104
|
+
severity: "major"
|
|
83105
|
+
});
|
|
83106
|
+
}
|
|
83107
|
+
if (!page.ok) {
|
|
83108
|
+
return {
|
|
83109
|
+
url: raw,
|
|
83110
|
+
reachable: false,
|
|
83111
|
+
score: scoreOf(issues),
|
|
83112
|
+
issues,
|
|
83113
|
+
error: page.error
|
|
83114
|
+
};
|
|
83115
|
+
}
|
|
83116
|
+
if (page.finalUrl.startsWith("http://") && !tlsResult.issue) {
|
|
83117
|
+
issues.push({
|
|
83118
|
+
code: "no_https_redirect",
|
|
83119
|
+
label: "The site falls back to an unencrypted connection instead of staying secure",
|
|
83120
|
+
severity: "major"
|
|
83121
|
+
});
|
|
83122
|
+
}
|
|
83123
|
+
issues.push(...markupIssues(page.html, page.finalUrl));
|
|
83124
|
+
const htmlKb = Math.round(page.html.length / 1024);
|
|
83125
|
+
if (htmlKb > 600) {
|
|
83126
|
+
issues.push({
|
|
83127
|
+
code: "heavy_page",
|
|
83128
|
+
label: "The page is unusually heavy, so it loads slowly on mobile data",
|
|
83129
|
+
severity: "minor",
|
|
83130
|
+
detail: `${htmlKb} KB of HTML`
|
|
83131
|
+
});
|
|
83132
|
+
}
|
|
83133
|
+
return { url: raw, reachable: true, score: scoreOf(issues), issues, htmlKb };
|
|
83134
|
+
}
|
|
83135
|
+
async function fetchPage(url) {
|
|
83136
|
+
try {
|
|
83137
|
+
const res = await axios_1.default.get(url, {
|
|
83138
|
+
timeout: 2e4,
|
|
83139
|
+
maxRedirects: 5,
|
|
83140
|
+
responseType: "text",
|
|
83141
|
+
// Some hosts serve a stripped page to a bare client.
|
|
83142
|
+
headers: {
|
|
83143
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
|
|
83144
|
+
Accept: "text/html,application/xhtml+xml"
|
|
83145
|
+
},
|
|
83146
|
+
maxContentLength: 5e6,
|
|
83147
|
+
validateStatus: () => true,
|
|
83148
|
+
// We want to inspect a broken-cert site's markup too; the cert problem is
|
|
83149
|
+
// reported separately by checkTls, so this doesn't hide anything.
|
|
83150
|
+
httpsAgent: void 0
|
|
83151
|
+
});
|
|
83152
|
+
if (res.status >= 400) {
|
|
83153
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
83154
|
+
}
|
|
83155
|
+
const html = typeof res.data === "string" ? res.data : String(res.data ?? "");
|
|
83156
|
+
if (!html)
|
|
83157
|
+
return { ok: false, error: "Empty response" };
|
|
83158
|
+
const finalUrl = res.request?.res?.responseUrl || url;
|
|
83159
|
+
return { ok: true, html, finalUrl };
|
|
83160
|
+
} catch (e) {
|
|
83161
|
+
return { ok: false, error: e.message };
|
|
83162
|
+
}
|
|
83163
|
+
}
|
|
83164
|
+
function scoreOf(issues) {
|
|
83165
|
+
const total = issues.reduce((sum, i) => sum + WEIGHT[i.severity], 0);
|
|
83166
|
+
return Math.min(100, total);
|
|
83167
|
+
}
|
|
83168
|
+
function isRebuildProspect(audit) {
|
|
83169
|
+
if (!audit.issues.length)
|
|
83170
|
+
return false;
|
|
83171
|
+
const critical = audit.issues.filter((i) => i.severity === "critical").length;
|
|
83172
|
+
const major = audit.issues.filter((i) => i.severity === "major").length;
|
|
83173
|
+
return critical >= 1 || major >= 2;
|
|
83174
|
+
}
|
|
83175
|
+
function headlineIssues(audit, limit = 3) {
|
|
83176
|
+
const rank = { critical: 0, major: 1, minor: 2 };
|
|
83177
|
+
return [...audit.issues].sort((a, b) => rank[a.severity] - rank[b.severity]).slice(0, limit);
|
|
83178
|
+
}
|
|
83179
|
+
}
|
|
83180
|
+
});
|
|
83181
|
+
|
|
82793
83182
|
// ../desktop/dist/main/lib/lead-search.js
|
|
82794
83183
|
var require_lead_search = __commonJS({
|
|
82795
83184
|
"../desktop/dist/main/lib/lead-search.js"(exports2) {
|
|
@@ -82800,11 +83189,14 @@ var require_lead_search = __commonJS({
|
|
|
82800
83189
|
var db_1 = require_db();
|
|
82801
83190
|
var places_1 = require_places();
|
|
82802
83191
|
var booking_detect_1 = require_booking_detect();
|
|
83192
|
+
var site_audit_1 = require_site_audit();
|
|
82803
83193
|
async function searchAndSaveLeadsStream(args, onLead) {
|
|
82804
83194
|
const db2 = (0, db_1.getDb)();
|
|
82805
83195
|
const cap = args.maxResults ?? 50;
|
|
82806
83196
|
const seen = /* @__PURE__ */ new Set();
|
|
82807
83197
|
const bookingMode = args.mode === "no_booking";
|
|
83198
|
+
const auditMode = args.mode === "outdated";
|
|
83199
|
+
const wantsSiteHavers = bookingMode || auditMode;
|
|
82808
83200
|
let saved = 0;
|
|
82809
83201
|
let found = 0;
|
|
82810
83202
|
await (0, places_1.searchPlacesStream)({
|
|
@@ -82813,8 +83205,8 @@ var require_lead_search = __commonJS({
|
|
|
82813
83205
|
types: args.types,
|
|
82814
83206
|
center: args.center,
|
|
82815
83207
|
radiusMeters: args.radiusMeters,
|
|
82816
|
-
// Booking
|
|
82817
|
-
includeWithWebsite:
|
|
83208
|
+
// Booking and audit modes need the businesses that DO have a website.
|
|
83209
|
+
includeWithWebsite: wantsSiteHavers
|
|
82818
83210
|
}, async (batch) => {
|
|
82819
83211
|
for (const r of batch) {
|
|
82820
83212
|
found++;
|
|
@@ -82829,7 +83221,19 @@ var require_lead_search = __commonJS({
|
|
|
82829
83221
|
let leadKind = "no_website";
|
|
82830
83222
|
let bookingStatus = null;
|
|
82831
83223
|
let bookingPlatform = null;
|
|
82832
|
-
|
|
83224
|
+
let siteScore = null;
|
|
83225
|
+
let siteIssues = null;
|
|
83226
|
+
if (auditMode) {
|
|
83227
|
+
if (!r.website)
|
|
83228
|
+
continue;
|
|
83229
|
+
args.onProgress?.(`Auditing ${r.name}'s website\u2026`);
|
|
83230
|
+
const audit = await (0, site_audit_1.auditSite)(r.website);
|
|
83231
|
+
if (!(0, site_audit_1.isRebuildProspect)(audit))
|
|
83232
|
+
continue;
|
|
83233
|
+
siteScore = audit.score;
|
|
83234
|
+
siteIssues = JSON.stringify(audit.issues);
|
|
83235
|
+
leadKind = "outdated";
|
|
83236
|
+
} else if (bookingMode) {
|
|
82833
83237
|
if (!r.website)
|
|
82834
83238
|
continue;
|
|
82835
83239
|
args.onProgress?.(`Checking ${r.name}'s booking setup\u2026`);
|
|
@@ -82843,8 +83247,8 @@ var require_lead_search = __commonJS({
|
|
|
82843
83247
|
continue;
|
|
82844
83248
|
}
|
|
82845
83249
|
db2.run(`INSERT INTO leads
|
|
82846
|
-
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status, lead_kind, booking_status, booking_platform)
|
|
82847
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered', ?, ?, ?)`, [
|
|
83250
|
+
(place_id, name, type, city, address, phone, website, rating, review_count, lat, lng, score, price_level, business_status, status, lead_kind, booking_status, booking_platform, site_score, site_issues, audited_at)
|
|
83251
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'discovered', ?, ?, ?, ?, ?, ?)`, [
|
|
82848
83252
|
r.place_id,
|
|
82849
83253
|
r.name,
|
|
82850
83254
|
r.type,
|
|
@@ -82861,7 +83265,10 @@ var require_lead_search = __commonJS({
|
|
|
82861
83265
|
r.business_status,
|
|
82862
83266
|
leadKind,
|
|
82863
83267
|
bookingStatus,
|
|
82864
|
-
bookingPlatform
|
|
83268
|
+
bookingPlatform,
|
|
83269
|
+
siteScore,
|
|
83270
|
+
siteIssues,
|
|
83271
|
+
siteScore === null ? null : (/* @__PURE__ */ new Date()).toISOString()
|
|
82865
83272
|
]);
|
|
82866
83273
|
const row = db2.get("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
82867
83274
|
saved++;
|
|
@@ -95258,6 +95665,7 @@ var require_chat_tools = __commonJS({
|
|
|
95258
95665
|
var lead_search_1 = require_lead_search();
|
|
95259
95666
|
var places_1 = require_places();
|
|
95260
95667
|
var booking_detect_1 = require_booking_detect();
|
|
95668
|
+
var site_audit_1 = require_site_audit();
|
|
95261
95669
|
var followup_1 = require_followup();
|
|
95262
95670
|
var invoicing_1 = require_invoicing();
|
|
95263
95671
|
var agency_1 = require_agency();
|
|
@@ -95449,6 +95857,44 @@ var require_chat_tools = __commonJS({
|
|
|
95449
95857
|
required: ["id"]
|
|
95450
95858
|
}
|
|
95451
95859
|
},
|
|
95860
|
+
{
|
|
95861
|
+
name: "audit_sites",
|
|
95862
|
+
description: "AUDIT existing websites in an area and save the bad ones as rebuild prospects. Unlike find_leads (which looks for businesses with NO website), this finds businesses that DO have a website and checks it for real, nameable faults: expired or missing HTTPS certificates, no mobile support, table layouts, Flash, obsolete jQuery, parked or under-construction pages, stale copyright years. Each prospect is scored (higher = worse) and its specific problems are stored so outreach can cite them. Use this when the user wants to find outdated, insecure or badly built websites to pitch a rebuild against. Costs the same Places lookup as find_leads plus one fetch per site.",
|
|
95863
|
+
input_schema: {
|
|
95864
|
+
type: "object",
|
|
95865
|
+
properties: {
|
|
95866
|
+
location: {
|
|
95867
|
+
type: "string",
|
|
95868
|
+
description: "Town, postcode, area or address to search, e.g. 'Leeds LS1' or 'Shoreditch, London'."
|
|
95869
|
+
},
|
|
95870
|
+
categories: {
|
|
95871
|
+
type: "array",
|
|
95872
|
+
items: { type: "string" },
|
|
95873
|
+
description: "Business types to search, e.g. ['barber shop','cafe','plumber']."
|
|
95874
|
+
},
|
|
95875
|
+
radiusMeters: {
|
|
95876
|
+
type: "number",
|
|
95877
|
+
description: "Search radius in metres. Defaults to 10000."
|
|
95878
|
+
},
|
|
95879
|
+
limit: {
|
|
95880
|
+
type: "number",
|
|
95881
|
+
description: "Maximum prospects to save. Defaults to 25."
|
|
95882
|
+
}
|
|
95883
|
+
},
|
|
95884
|
+
required: ["location", "categories"]
|
|
95885
|
+
}
|
|
95886
|
+
},
|
|
95887
|
+
{
|
|
95888
|
+
name: "audit_one_site",
|
|
95889
|
+
description: "Audit a SINGLE website by URL and report its faults, without saving anything. Use this when the user names a specific site and asks what's wrong with it, or to check a business's site before pitching them.",
|
|
95890
|
+
input_schema: {
|
|
95891
|
+
type: "object",
|
|
95892
|
+
properties: {
|
|
95893
|
+
url: { type: "string", description: "The website to audit, e.g. 'thebarbers.co.uk'." },
|
|
95894
|
+
id: { type: "number", description: "Or a lead id \u2014 audits that lead's website and stores the result." }
|
|
95895
|
+
}
|
|
95896
|
+
}
|
|
95897
|
+
},
|
|
95452
95898
|
{
|
|
95453
95899
|
name: "hand_over_site",
|
|
95454
95900
|
description: "HAND OVER a finished site to the paying client: removes the DEMO/preview banner, optionally attaches the client's own domain, and returns every credential in one place (site URL, /admin panel + password, booking page + its password, and the DNS records the client must set at their registrar). This is the paid-deliverable step \u2014 the banner exists to stop clients using a free demo as their real site, so only run this when the user says the client has paid / signed off / is going live. Requires the lead to already have a built site or booking page.",
|
|
@@ -96241,6 +96687,77 @@ Interested?`;
|
|
|
96241
96687
|
return { ok: false, error: stats.error, business: lead.name };
|
|
96242
96688
|
return { ok: true, id, business: lead.name, url: base, ...stats.data };
|
|
96243
96689
|
}
|
|
96690
|
+
case "audit_sites": {
|
|
96691
|
+
const location = String(input.location ?? "").trim();
|
|
96692
|
+
const categories = input.categories ?? [];
|
|
96693
|
+
if (!location || categories.length === 0) {
|
|
96694
|
+
return { ok: false, error: "Give me a location and at least one business category." };
|
|
96695
|
+
}
|
|
96696
|
+
const placesKey = getDecryptedSetting("places_key");
|
|
96697
|
+
if (!placesKey)
|
|
96698
|
+
throw new Error("Google Places API key not set in Settings");
|
|
96699
|
+
ctx.emitProgress?.({
|
|
96700
|
+
channel: "leads:searching",
|
|
96701
|
+
payload: { msg: `Auditing websites in ${location}\u2026` }
|
|
96702
|
+
});
|
|
96703
|
+
const matches = await (0, places_1.geocodeForward)(location, 1);
|
|
96704
|
+
const place = matches[0];
|
|
96705
|
+
const result = await (0, lead_search_1.searchAndSaveLeadsStream)({
|
|
96706
|
+
placesKey,
|
|
96707
|
+
city: location,
|
|
96708
|
+
types: categories,
|
|
96709
|
+
maxResults: Math.min(Number(input.limit ?? 25), 100),
|
|
96710
|
+
center: place ? { lat: place.lat, lng: place.lng } : void 0,
|
|
96711
|
+
radiusMeters: Number(input.radiusMeters ?? 1e4),
|
|
96712
|
+
mode: "outdated"
|
|
96713
|
+
}, (lead) => ctx.emitProgress?.({ channel: "leads:found", payload: { lead } }));
|
|
96714
|
+
ctx.emitProgress?.({ channel: "leads:changed", payload: { reason: "audit_sites" } });
|
|
96715
|
+
const saved = db2.all(`SELECT id, name, website, site_score, site_issues
|
|
96716
|
+
FROM leads
|
|
96717
|
+
WHERE lead_kind = 'outdated' AND audited_at IS NOT NULL
|
|
96718
|
+
ORDER BY audited_at DESC, site_score DESC
|
|
96719
|
+
LIMIT ?`, [Number(input.limit ?? 25)]);
|
|
96720
|
+
return {
|
|
96721
|
+
ok: true,
|
|
96722
|
+
found: result.found,
|
|
96723
|
+
saved: result.saved,
|
|
96724
|
+
prospects: saved.map((r) => ({
|
|
96725
|
+
id: r.id,
|
|
96726
|
+
business: r.name,
|
|
96727
|
+
website: r.website,
|
|
96728
|
+
score: r.site_score,
|
|
96729
|
+
issues: JSON.parse(r.site_issues || "[]").slice(0, 3).map((i) => `${i.severity}: ${i.label}`)
|
|
96730
|
+
}))
|
|
96731
|
+
};
|
|
96732
|
+
}
|
|
96733
|
+
case "audit_one_site": {
|
|
96734
|
+
let url = input.url ?? "";
|
|
96735
|
+
let leadId = null;
|
|
96736
|
+
if (input.id != null) {
|
|
96737
|
+
leadId = Number(input.id);
|
|
96738
|
+
const lead = db2.get("SELECT id, name, website FROM leads WHERE id = ?", [leadId]);
|
|
96739
|
+
if (!lead)
|
|
96740
|
+
throw new Error(`Lead #${leadId} not found`);
|
|
96741
|
+
if (!lead.website)
|
|
96742
|
+
return { ok: false, error: `#${leadId} ${lead.name} has no website on record.` };
|
|
96743
|
+
url = lead.website;
|
|
96744
|
+
}
|
|
96745
|
+
if (!url)
|
|
96746
|
+
return { ok: false, error: "Give me a website URL or a lead id." };
|
|
96747
|
+
const audit = await (0, site_audit_1.auditSite)(url);
|
|
96748
|
+
if (leadId !== null) {
|
|
96749
|
+
db2.run("UPDATE leads SET site_score = ?, site_issues = ?, audited_at = ? WHERE id = ?", [audit.score, JSON.stringify(audit.issues), (/* @__PURE__ */ new Date()).toISOString(), leadId]);
|
|
96750
|
+
}
|
|
96751
|
+
return {
|
|
96752
|
+
ok: true,
|
|
96753
|
+
url: audit.url,
|
|
96754
|
+
reachable: audit.reachable,
|
|
96755
|
+
score: audit.score,
|
|
96756
|
+
worthRebuilding: (0, site_audit_1.isRebuildProspect)(audit),
|
|
96757
|
+
issues: audit.issues,
|
|
96758
|
+
error: audit.error
|
|
96759
|
+
};
|
|
96760
|
+
}
|
|
96244
96761
|
case "hand_over_site": {
|
|
96245
96762
|
const id = Number(input.id);
|
|
96246
96763
|
const lead = db2.get("SELECT id, name, site_url, booking_url FROM leads WHERE id = ?", [id]);
|
|
@@ -97274,7 +97791,7 @@ var require_package2 = __commonJS({
|
|
|
97274
97791
|
"package.json"(exports2, module2) {
|
|
97275
97792
|
module2.exports = {
|
|
97276
97793
|
name: "sproutkit-code",
|
|
97277
|
-
version: "1.
|
|
97794
|
+
version: "1.8.0",
|
|
97278
97795
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
97279
97796
|
bin: {
|
|
97280
97797
|
sproutkit: "./dist/index.js"
|
|
@@ -99146,6 +99663,135 @@ async function cmdReport(argv) {
|
|
|
99146
99663
|
process.exitCode = 1;
|
|
99147
99664
|
}
|
|
99148
99665
|
}
|
|
99666
|
+
async function cmdAudit(argv) {
|
|
99667
|
+
const { flags, rest } = parseArgs(argv);
|
|
99668
|
+
const json = !!flags.json;
|
|
99669
|
+
const SEV_COLOUR = {
|
|
99670
|
+
critical: c.red,
|
|
99671
|
+
major: c.amber,
|
|
99672
|
+
minor: c.dim
|
|
99673
|
+
};
|
|
99674
|
+
const asId = Number(flags.id ?? rest[0]);
|
|
99675
|
+
const singleUrl = flags.url ? String(flags.url) : void 0;
|
|
99676
|
+
const looksLikeUrl = singleUrl || rest[0] && /\./.test(String(rest[0])) && !flags.categories;
|
|
99677
|
+
if (singleUrl || !Number.isNaN(asId) && rest.length === 1 && !flags.categories || looksLikeUrl) {
|
|
99678
|
+
const status2 = json ? void 0 : new Status();
|
|
99679
|
+
const target = singleUrl ?? (Number.isNaN(asId) ? String(rest[0]) : `lead #${asId}`);
|
|
99680
|
+
if (status2) status2.start(`Auditing ${target}`);
|
|
99681
|
+
try {
|
|
99682
|
+
const r = await runTool(
|
|
99683
|
+
"audit_one_site",
|
|
99684
|
+
Number.isNaN(asId) || singleUrl ? { url: singleUrl ?? String(rest[0]) } : { id: asId },
|
|
99685
|
+
makeCtx({ json, status: status2 })
|
|
99686
|
+
);
|
|
99687
|
+
if (r && r.ok === false) {
|
|
99688
|
+
if (status2) status2.done(false, r.error ?? "Couldn't audit it");
|
|
99689
|
+
if (json) emitJson(r);
|
|
99690
|
+
process.exitCode = 1;
|
|
99691
|
+
return;
|
|
99692
|
+
}
|
|
99693
|
+
if (status2) {
|
|
99694
|
+
status2.done(
|
|
99695
|
+
true,
|
|
99696
|
+
r.issues && r.issues.length ? `${r.issues.length} issue${r.issues.length === 1 ? "" : "s"} \xB7 score ${r.score}` : "No problems found"
|
|
99697
|
+
);
|
|
99698
|
+
}
|
|
99699
|
+
if (json) {
|
|
99700
|
+
emitJson(r);
|
|
99701
|
+
return;
|
|
99702
|
+
}
|
|
99703
|
+
say();
|
|
99704
|
+
say(` ${c.bold(String(r.url))}` + c.dim(` \xB7 score ${r.score}/100`));
|
|
99705
|
+
if (!r.issues?.length) {
|
|
99706
|
+
say(c.dim(" Nothing worth pitching against \u2014 this site is in decent shape."));
|
|
99707
|
+
say();
|
|
99708
|
+
return;
|
|
99709
|
+
}
|
|
99710
|
+
say();
|
|
99711
|
+
for (const i of r.issues) {
|
|
99712
|
+
const tint = SEV_COLOUR[i.severity] ?? c.dim;
|
|
99713
|
+
say(` ${tint("\u25CF")} ${i.label}${i.detail ? c.dim(` (${i.detail})`) : ""}`);
|
|
99714
|
+
}
|
|
99715
|
+
say();
|
|
99716
|
+
say(
|
|
99717
|
+
r.worthRebuilding ? c.green(" Worth approaching \u2014 these are concrete problems you can name.") : c.dim(" Minor only. Probably not worth a rebuild pitch.")
|
|
99718
|
+
);
|
|
99719
|
+
say();
|
|
99720
|
+
} catch (e) {
|
|
99721
|
+
if (status2) status2.done(false, "Audit failed");
|
|
99722
|
+
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
99723
|
+
process.exitCode = 1;
|
|
99724
|
+
}
|
|
99725
|
+
return;
|
|
99726
|
+
}
|
|
99727
|
+
const location = rest.join(" ").trim() || (flags.location ? String(flags.location) : "");
|
|
99728
|
+
if (!location) {
|
|
99729
|
+
const err = 'Usage: sproutkit audit "<area>" --categories "barber shop,cafe"\n single site: sproutkit audit <leadId> \xB7 sproutkit audit --url thebarbers.co.uk';
|
|
99730
|
+
json ? emitJson({ ok: false, error: err }) : say(c.red(" " + err));
|
|
99731
|
+
process.exitCode = 1;
|
|
99732
|
+
return;
|
|
99733
|
+
}
|
|
99734
|
+
const categories = flags.categories ? String(flags.categories).split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
99735
|
+
if (categories.length === 0) {
|
|
99736
|
+
const err = 'Which trades? e.g. --categories "barber shop,cafe,plumber"';
|
|
99737
|
+
json ? emitJson({ ok: false, error: err }) : say(c.red(" " + err));
|
|
99738
|
+
process.exitCode = 1;
|
|
99739
|
+
return;
|
|
99740
|
+
}
|
|
99741
|
+
const status = json ? void 0 : new Status();
|
|
99742
|
+
if (status) status.start(`Auditing websites in ${location}`);
|
|
99743
|
+
try {
|
|
99744
|
+
const r = await runTool(
|
|
99745
|
+
"audit_sites",
|
|
99746
|
+
{
|
|
99747
|
+
location,
|
|
99748
|
+
categories,
|
|
99749
|
+
radiusMeters: flags.radius ? Number(flags.radius) : void 0,
|
|
99750
|
+
limit: flags.limit ? Number(flags.limit) : void 0
|
|
99751
|
+
},
|
|
99752
|
+
makeCtx({ json, status })
|
|
99753
|
+
);
|
|
99754
|
+
if (r && r.ok === false) {
|
|
99755
|
+
if (status) status.done(false, r.error ?? "Audit failed");
|
|
99756
|
+
if (json) emitJson(r);
|
|
99757
|
+
process.exitCode = 1;
|
|
99758
|
+
return;
|
|
99759
|
+
}
|
|
99760
|
+
if (status) status.done(true, `${r.saved} rebuild prospect${r.saved === 1 ? "" : "s"} from ${r.found} checked`);
|
|
99761
|
+
autoSync();
|
|
99762
|
+
if (json) {
|
|
99763
|
+
emitJson(r);
|
|
99764
|
+
return;
|
|
99765
|
+
}
|
|
99766
|
+
const list = r.prospects ?? [];
|
|
99767
|
+
if (!list.length) {
|
|
99768
|
+
say();
|
|
99769
|
+
say(c.dim(` Checked ${r.found} businesses \u2014 none had a site bad enough to pitch against.`));
|
|
99770
|
+
say(c.dim(" That's a good sign for the area; try a different trade or a wider radius."));
|
|
99771
|
+
say();
|
|
99772
|
+
return;
|
|
99773
|
+
}
|
|
99774
|
+
say();
|
|
99775
|
+
say(c.dim(" Worst first \u2014 these are the easiest conversations to start."));
|
|
99776
|
+
say();
|
|
99777
|
+
for (const p of list) {
|
|
99778
|
+
say(` ${c.greenBold("#" + p.id)} ${c.bold(p.business)} ${c.dim(p.website.replace(/^https?:\/\//, ""))}`);
|
|
99779
|
+
say(` ${c.amber("score " + p.score + "/100")}`);
|
|
99780
|
+
for (const issue of p.issues) {
|
|
99781
|
+
const [sev, ...restText] = issue.split(": ");
|
|
99782
|
+
const tint = SEV_COLOUR[sev] ?? c.dim;
|
|
99783
|
+
say(` ${tint("\u25CF")} ${restText.join(": ")}`);
|
|
99784
|
+
}
|
|
99785
|
+
say();
|
|
99786
|
+
}
|
|
99787
|
+
say(c.dim(" Build one: sproutkit build <id> \xB7 see everything: sproutkit audit <id>"));
|
|
99788
|
+
say();
|
|
99789
|
+
} catch (e) {
|
|
99790
|
+
if (status) status.done(false, "Audit failed");
|
|
99791
|
+
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
99792
|
+
process.exitCode = 1;
|
|
99793
|
+
}
|
|
99794
|
+
}
|
|
99149
99795
|
async function cmdRegenerate(argv) {
|
|
99150
99796
|
const { flags, rest } = parseArgs(argv);
|
|
99151
99797
|
const json = !!flags.json;
|
|
@@ -99408,6 +100054,8 @@ When a request is missing information the command needs, ask a SHORT, STRUCTURED
|
|
|
99408
100054
|
COMMANDS:
|
|
99409
100055
|
- sproutkit find "<area>" --categories "<type>,<type>" --json \u2014 find website-less leads
|
|
99410
100056
|
- sproutkit find "<area>" --categories "\u2026" --booking --json \u2014 find businesses that HAVE a site but take bookings via WhatsApp/phone only (booking-page prospects)
|
|
100057
|
+
- sproutkit audit "<area>" --categories "\u2026" --json \u2014 find businesses whose EXISTING site is insecure/outdated
|
|
100058
|
+
- sproutkit audit <id> | --url <site> --json \u2014 audit one site, list its faults
|
|
99411
100059
|
- sproutkit list [--status <s>] [--city <c>] [--limit 100] \u2014 formatted table; SHOW IT VERBATIM
|
|
99412
100060
|
- sproutkit list --json \u2014 same data as JSON, for reading ids only
|
|
99413
100061
|
- sproutkit build <id> --json [--instructions "..."] [--cms] [--multipage] \u2014 build a demo site (--multipage = real Home/Services/About/Contact pages, not one scroll)
|
|
@@ -99607,7 +100255,7 @@ async function main() {
|
|
|
99607
100255
|
closeTty();
|
|
99608
100256
|
return;
|
|
99609
100257
|
}
|
|
99610
|
-
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "handover" || arg === "brand" || arg === "invoice" || arg === "letter" || arg === "reply" || arg === "followups" || arg === "report" || arg === "outreach" || arg === "pipeline") {
|
|
100258
|
+
if (arg === "find" || arg === "list" || arg === "build" || arg === "build-all" || arg === "regenerate" || arg === "cms" || arg === "book" || arg === "handover" || arg === "audit" || arg === "brand" || arg === "invoice" || arg === "letter" || arg === "reply" || arg === "followups" || arg === "report" || arg === "outreach" || arg === "pipeline") {
|
|
99611
100259
|
const subArgs = process.argv.slice(3);
|
|
99612
100260
|
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
99613
100261
|
const USAGE = {
|
|
@@ -99647,6 +100295,16 @@ async function main() {
|
|
|
99647
100295
|
' --instructions "\u2026" e.g. "45-min slots, add beard trim \xA312"',
|
|
99648
100296
|
" --email <addr> owner email for booking notifications"
|
|
99649
100297
|
],
|
|
100298
|
+
audit: [
|
|
100299
|
+
'sproutkit audit "<area>" --categories "barber shop,cafe"',
|
|
100300
|
+
" finds businesses that HAVE a website but whose site is costing them",
|
|
100301
|
+
" work \u2014 no HTTPS, unusable on phones, built in 2008 \u2014 and scores it",
|
|
100302
|
+
" --radius <m> search radius (default 10000)",
|
|
100303
|
+
" --limit <n> max prospects to save (default 25)",
|
|
100304
|
+
"",
|
|
100305
|
+
" one site: sproutkit audit <leadId>",
|
|
100306
|
+
" sproutkit audit --url thebarbers.co.uk"
|
|
100307
|
+
],
|
|
99650
100308
|
brand: [
|
|
99651
100309
|
'sproutkit brand --name "Your Studio" --email you@x.com [--vat-rate 20]',
|
|
99652
100310
|
" your agency identity \u2014 goes on invoices, letters, the site footer",
|
|
@@ -99724,6 +100382,7 @@ async function main() {
|
|
|
99724
100382
|
else if (arg === "cms") await cmdCms(subArgs);
|
|
99725
100383
|
else if (arg === "book") await cmdBook(subArgs);
|
|
99726
100384
|
else if (arg === "handover") await cmdHandover(subArgs);
|
|
100385
|
+
else if (arg === "audit") await cmdAudit(subArgs);
|
|
99727
100386
|
else if (arg === "brand") await cmdBrand(subArgs);
|
|
99728
100387
|
else if (arg === "invoice") await cmdInvoice(subArgs);
|
|
99729
100388
|
else if (arg === "letter") await cmdLetter(subArgs);
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -93,6 +93,17 @@ you can parse results reliably, then summarise them for the user in prose.
|
|
|
93
93
|
- The banner exists to stop clients using a free demo as their real site, so
|
|
94
94
|
do NOT run this just because a site looks finished.
|
|
95
95
|
|
|
96
|
+
- **Audit existing websites** — `sproutkit audit "<area>" --categories "barber shop,cafe" --json`
|
|
97
|
+
- The counterpart to `find`. Where `find` looks for the ABSENCE of a website,
|
|
98
|
+
`audit` looks at sites that DO exist and finds the ones costing their owner
|
|
99
|
+
work: expired or missing HTTPS, no mobile support, table layouts, Flash,
|
|
100
|
+
obsolete jQuery, parked/under-construction pages, stale copyright years.
|
|
101
|
+
- Each prospect is scored (higher = worse) and its specific faults are stored,
|
|
102
|
+
so outreach can name real problems rather than say "your site looks dated".
|
|
103
|
+
- Single site: `sproutkit audit <leadId>` or `sproutkit audit --url thebarbers.co.uk`.
|
|
104
|
+
- Sites with only minor issues are deliberately skipped — pitching a rebuild
|
|
105
|
+
over a missing meta description makes the agency look desperate.
|
|
106
|
+
|
|
96
107
|
- **Set up the agency's identity** — `sproutkit brand --show`
|
|
97
108
|
- Their name, logo, address, VAT and bank details. This is what appears on
|
|
98
109
|
invoices and letters, in the site footer credit, on the demo banner and in
|