sproutkit-code 1.9.1 → 1.10.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 +635 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -96,7 +96,8 @@ var require_runtime = __commonJS({
|
|
|
96
96
|
"resend_key",
|
|
97
97
|
"report_key",
|
|
98
98
|
"smtp_pass",
|
|
99
|
-
"imap_pass"
|
|
99
|
+
"imap_pass",
|
|
100
|
+
"stripe_secret_key"
|
|
100
101
|
]);
|
|
101
102
|
function isSecretSetting(key) {
|
|
102
103
|
return SECRET_SETTINGS.has(key);
|
|
@@ -314,6 +315,84 @@ var require_db = __commonJS({
|
|
|
314
315
|
last_result TEXT,
|
|
315
316
|
created_at TEXT DEFAULT (datetime('now'))
|
|
316
317
|
);
|
|
318
|
+
`);
|
|
319
|
+
try {
|
|
320
|
+
db2.exec("ALTER TABLE schedules ADD COLUMN config TEXT");
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
db2.exec(`
|
|
324
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
325
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
326
|
+
lead_id INTEGER NOT NULL,
|
|
327
|
+
direction TEXT NOT NULL,
|
|
328
|
+
from_addr TEXT,
|
|
329
|
+
to_addr TEXT,
|
|
330
|
+
subject TEXT,
|
|
331
|
+
body TEXT,
|
|
332
|
+
message_id TEXT UNIQUE,
|
|
333
|
+
in_reply_to TEXT,
|
|
334
|
+
at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
335
|
+
intent TEXT,
|
|
336
|
+
intent_summary TEXT,
|
|
337
|
+
confidence REAL,
|
|
338
|
+
handled TEXT NOT NULL DEFAULT 'new',
|
|
339
|
+
draft_subject TEXT,
|
|
340
|
+
draft_body TEXT,
|
|
341
|
+
sent_by TEXT
|
|
342
|
+
);
|
|
343
|
+
`);
|
|
344
|
+
try {
|
|
345
|
+
db2.exec("CREATE INDEX IF NOT EXISTS idx_messages_lead ON messages(lead_id, at)");
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
db2.exec(`
|
|
350
|
+
INSERT INTO messages (lead_id, direction, from_addr, body, at, handled)
|
|
351
|
+
SELECT id, 'in', email, last_reply_snippet, COALESCE(last_reply_at, replied_at, datetime('now')), 'done'
|
|
352
|
+
FROM leads
|
|
353
|
+
WHERE last_reply_snippet IS NOT NULL AND last_reply_snippet != ''
|
|
354
|
+
AND NOT EXISTS (SELECT 1 FROM messages m WHERE m.lead_id = leads.id)
|
|
355
|
+
`);
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
db2.exec(`
|
|
359
|
+
CREATE TABLE IF NOT EXISTS do_not_contact (
|
|
360
|
+
email TEXT PRIMARY KEY,
|
|
361
|
+
lead_id INTEGER,
|
|
362
|
+
reason TEXT,
|
|
363
|
+
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
364
|
+
);
|
|
365
|
+
`);
|
|
366
|
+
db2.exec(`
|
|
367
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
368
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
369
|
+
at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
370
|
+
actor TEXT NOT NULL,
|
|
371
|
+
action TEXT NOT NULL,
|
|
372
|
+
lead_id INTEGER,
|
|
373
|
+
detail TEXT,
|
|
374
|
+
needs_you INTEGER NOT NULL DEFAULT 0
|
|
375
|
+
);
|
|
376
|
+
`);
|
|
377
|
+
db2.exec(`
|
|
378
|
+
CREATE TABLE IF NOT EXISTS deals (
|
|
379
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
380
|
+
lead_id INTEGER NOT NULL,
|
|
381
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
382
|
+
site_pence INTEGER NOT NULL,
|
|
383
|
+
care_pence INTEGER NOT NULL DEFAULT 0,
|
|
384
|
+
currency TEXT NOT NULL,
|
|
385
|
+
payment_link_id TEXT,
|
|
386
|
+
payment_url TEXT,
|
|
387
|
+
checkout_session_id TEXT,
|
|
388
|
+
stripe_customer_id TEXT,
|
|
389
|
+
subscription_id TEXT,
|
|
390
|
+
care_status TEXT,
|
|
391
|
+
invoice_number TEXT,
|
|
392
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
393
|
+
paid_at TEXT,
|
|
394
|
+
handed_over_at TEXT
|
|
395
|
+
);
|
|
317
396
|
`);
|
|
318
397
|
}
|
|
319
398
|
function getDb2() {
|
|
@@ -30587,12 +30666,31 @@ var require_llm = __commonJS({
|
|
|
30587
30666
|
backend: "api"
|
|
30588
30667
|
};
|
|
30589
30668
|
}
|
|
30669
|
+
var STALE_MODEL = /issue with the selected model|model .*(does not exist|not found|may not exist)|invalid model/i;
|
|
30670
|
+
var FALLBACK_ALIASES = ["sonnet", "opus", "haiku"];
|
|
30590
30671
|
async function generateViaClaudeCode(args) {
|
|
30672
|
+
const preferred = getClaudeCodeModel2();
|
|
30673
|
+
try {
|
|
30674
|
+
return await runClaudeCode(args, preferred);
|
|
30675
|
+
} catch (e) {
|
|
30676
|
+
if (!STALE_MODEL.test(e.message))
|
|
30677
|
+
throw e;
|
|
30678
|
+
for (const alias of FALLBACK_ALIASES.filter((a) => a !== preferred)) {
|
|
30679
|
+
try {
|
|
30680
|
+
return await runClaudeCode(args, alias);
|
|
30681
|
+
} catch (e2) {
|
|
30682
|
+
if (!STALE_MODEL.test(e2.message))
|
|
30683
|
+
throw e2;
|
|
30684
|
+
}
|
|
30685
|
+
}
|
|
30686
|
+
throw e;
|
|
30687
|
+
}
|
|
30688
|
+
}
|
|
30689
|
+
async function runClaudeCode(args, alias) {
|
|
30591
30690
|
if (args.images && args.images.length > 0) {
|
|
30592
30691
|
throw new Error("Image input isn't supported on the Claude subscription backend. This step needs an Anthropic API key.");
|
|
30593
30692
|
}
|
|
30594
30693
|
const cliArgs = ["-p", "--output-format", "json"];
|
|
30595
|
-
const alias = getClaudeCodeModel2();
|
|
30596
30694
|
if (alias)
|
|
30597
30695
|
cliArgs.push("--model", alias);
|
|
30598
30696
|
if (args.allowWebSearch)
|
|
@@ -95611,7 +95709,9 @@ var require_email = __commonJS({
|
|
|
95611
95709
|
from: cfg.from,
|
|
95612
95710
|
to: args.to,
|
|
95613
95711
|
subject: args.subject,
|
|
95614
|
-
text: args.text
|
|
95712
|
+
text: args.text,
|
|
95713
|
+
...args.inReplyTo ? { inReplyTo: args.inReplyTo } : {},
|
|
95714
|
+
...args.references ? { references: args.references } : {}
|
|
95615
95715
|
});
|
|
95616
95716
|
return { ok: true, messageId: info.messageId };
|
|
95617
95717
|
} catch (e) {
|
|
@@ -95706,6 +95806,487 @@ Respond with ONLY compact JSON, no prose: {"email": string|null}`;
|
|
|
95706
95806
|
}
|
|
95707
95807
|
});
|
|
95708
95808
|
|
|
95809
|
+
// ../desktop/dist/main/lib/guardrails.js
|
|
95810
|
+
var require_guardrails = __commonJS({
|
|
95811
|
+
"../desktop/dist/main/lib/guardrails.js"(exports2) {
|
|
95812
|
+
"use strict";
|
|
95813
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
95814
|
+
exports2.getAutopilotSettings = getAutopilotSettings;
|
|
95815
|
+
exports2.setAutopilotSettings = setAutopilotSettings;
|
|
95816
|
+
exports2.emailsSentToday = emailsSentToday;
|
|
95817
|
+
exports2.sendBlockedReason = sendBlockedReason;
|
|
95818
|
+
exports2.isSuppressed = isSuppressed;
|
|
95819
|
+
exports2.suppress = suppress;
|
|
95820
|
+
exports2.unsuppress = unsuppress;
|
|
95821
|
+
exports2.listSuppressed = listSuppressed;
|
|
95822
|
+
exports2.logActivity = logActivity;
|
|
95823
|
+
exports2.listActivity = listActivity;
|
|
95824
|
+
exports2.clearNeedsYou = clearNeedsYou;
|
|
95825
|
+
var db_1 = require_db();
|
|
95826
|
+
var DEFAULTS = {
|
|
95827
|
+
paused: false,
|
|
95828
|
+
replyMode: "draft",
|
|
95829
|
+
sendProposals: true,
|
|
95830
|
+
handoverMode: "ask",
|
|
95831
|
+
maxEmailsPerDay: 30,
|
|
95832
|
+
minConfidence: 0.8,
|
|
95833
|
+
sitePence: 19900,
|
|
95834
|
+
carePence: 0,
|
|
95835
|
+
callUrl: null
|
|
95836
|
+
};
|
|
95837
|
+
function read(key) {
|
|
95838
|
+
const row = (0, db_1.getDb)().get("SELECT value FROM settings WHERE key = ?", [key]);
|
|
95839
|
+
return row?.value ?? null;
|
|
95840
|
+
}
|
|
95841
|
+
function write(key, value) {
|
|
95842
|
+
(0, db_1.getDb)().run("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [key, value]);
|
|
95843
|
+
}
|
|
95844
|
+
var num = (v, d) => v != null && v !== "" && !Number.isNaN(Number(v)) ? Number(v) : d;
|
|
95845
|
+
function getAutopilotSettings() {
|
|
95846
|
+
const mode = read("auto_reply_mode");
|
|
95847
|
+
return {
|
|
95848
|
+
paused: read("autopilot_paused") === "1",
|
|
95849
|
+
replyMode: mode === "off" || mode === "auto" || mode === "draft" ? mode : DEFAULTS.replyMode,
|
|
95850
|
+
sendProposals: read("auto_send_proposals") !== "0",
|
|
95851
|
+
handoverMode: read("auto_handover_mode") === "auto" ? "auto" : "ask",
|
|
95852
|
+
maxEmailsPerDay: num(read("cap_emails_per_day"), DEFAULTS.maxEmailsPerDay),
|
|
95853
|
+
minConfidence: num(read("auto_min_confidence"), DEFAULTS.minConfidence),
|
|
95854
|
+
sitePence: num(read("price_site_pence"), DEFAULTS.sitePence),
|
|
95855
|
+
carePence: num(read("price_care_pence"), DEFAULTS.carePence),
|
|
95856
|
+
callUrl: read("call_booking_url") || null
|
|
95857
|
+
};
|
|
95858
|
+
}
|
|
95859
|
+
function setAutopilotSettings(patch) {
|
|
95860
|
+
if (patch.paused !== void 0)
|
|
95861
|
+
write("autopilot_paused", patch.paused ? "1" : "0");
|
|
95862
|
+
if (patch.replyMode)
|
|
95863
|
+
write("auto_reply_mode", patch.replyMode);
|
|
95864
|
+
if (patch.sendProposals !== void 0)
|
|
95865
|
+
write("auto_send_proposals", patch.sendProposals ? "1" : "0");
|
|
95866
|
+
if (patch.handoverMode)
|
|
95867
|
+
write("auto_handover_mode", patch.handoverMode);
|
|
95868
|
+
if (patch.maxEmailsPerDay !== void 0)
|
|
95869
|
+
write("cap_emails_per_day", String(Math.max(0, Math.floor(patch.maxEmailsPerDay))));
|
|
95870
|
+
if (patch.minConfidence !== void 0)
|
|
95871
|
+
write("auto_min_confidence", String(Math.min(1, Math.max(0.5, patch.minConfidence))));
|
|
95872
|
+
if (patch.sitePence !== void 0)
|
|
95873
|
+
write("price_site_pence", String(Math.max(0, Math.round(patch.sitePence))));
|
|
95874
|
+
if (patch.carePence !== void 0)
|
|
95875
|
+
write("price_care_pence", String(Math.max(0, Math.round(patch.carePence))));
|
|
95876
|
+
if (patch.callUrl !== void 0)
|
|
95877
|
+
write("call_booking_url", patch.callUrl ?? "");
|
|
95878
|
+
return getAutopilotSettings();
|
|
95879
|
+
}
|
|
95880
|
+
function emailsSentToday() {
|
|
95881
|
+
const row = (0, db_1.getDb)().get(`SELECT COUNT(*) AS n FROM messages
|
|
95882
|
+
WHERE direction = 'out' AND date(at) = date('now')`, []);
|
|
95883
|
+
return row?.n ?? 0;
|
|
95884
|
+
}
|
|
95885
|
+
function sendBlockedReason() {
|
|
95886
|
+
const s = getAutopilotSettings();
|
|
95887
|
+
if (s.paused)
|
|
95888
|
+
return "Autopilot is paused.";
|
|
95889
|
+
const sent = emailsSentToday();
|
|
95890
|
+
if (sent >= s.maxEmailsPerDay)
|
|
95891
|
+
return `Daily email limit reached (${sent}/${s.maxEmailsPerDay}).`;
|
|
95892
|
+
return null;
|
|
95893
|
+
}
|
|
95894
|
+
var norm = (e) => e.trim().toLowerCase();
|
|
95895
|
+
function isSuppressed(email) {
|
|
95896
|
+
if (!email)
|
|
95897
|
+
return false;
|
|
95898
|
+
return !!(0, db_1.getDb)().get("SELECT 1 FROM do_not_contact WHERE email = ?", [norm(email)]);
|
|
95899
|
+
}
|
|
95900
|
+
function suppress(email, reason, leadId) {
|
|
95901
|
+
(0, db_1.getDb)().run(`INSERT INTO do_not_contact (email, lead_id, reason) VALUES (?, ?, ?)
|
|
95902
|
+
ON CONFLICT(email) DO UPDATE SET reason = excluded.reason`, [norm(email), leadId ?? null, reason]);
|
|
95903
|
+
}
|
|
95904
|
+
function unsuppress(email) {
|
|
95905
|
+
(0, db_1.getDb)().run("DELETE FROM do_not_contact WHERE email = ?", [norm(email)]);
|
|
95906
|
+
}
|
|
95907
|
+
function listSuppressed() {
|
|
95908
|
+
return (0, db_1.getDb)().all("SELECT email, lead_id, reason, at FROM do_not_contact ORDER BY at DESC", []);
|
|
95909
|
+
}
|
|
95910
|
+
function logActivity(args) {
|
|
95911
|
+
(0, db_1.getDb)().run("INSERT INTO activity_log (actor, action, lead_id, detail, needs_you) VALUES (?, ?, ?, ?, ?)", [args.actor ?? "autopilot", args.action, args.leadId ?? null, args.detail ?? null, args.needsYou ? 1 : 0]);
|
|
95912
|
+
}
|
|
95913
|
+
function listActivity(limit = 100) {
|
|
95914
|
+
return (0, db_1.getDb)().all(`SELECT a.*, l.name AS lead_name FROM activity_log a
|
|
95915
|
+
LEFT JOIN leads l ON l.id = a.lead_id
|
|
95916
|
+
ORDER BY a.id DESC LIMIT ?`, [limit]);
|
|
95917
|
+
}
|
|
95918
|
+
function clearNeedsYou(id) {
|
|
95919
|
+
(0, db_1.getDb)().run("UPDATE activity_log SET needs_you = 0 WHERE id = ?", [id]);
|
|
95920
|
+
}
|
|
95921
|
+
}
|
|
95922
|
+
});
|
|
95923
|
+
|
|
95924
|
+
// ../desktop/dist/main/lib/inbox.js
|
|
95925
|
+
var require_inbox = __commonJS({
|
|
95926
|
+
"../desktop/dist/main/lib/inbox.js"(exports2) {
|
|
95927
|
+
"use strict";
|
|
95928
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
95929
|
+
exports2.recordOutgoing = recordOutgoing;
|
|
95930
|
+
exports2.recordIncoming = recordIncoming;
|
|
95931
|
+
exports2.listThreads = listThreads;
|
|
95932
|
+
exports2.getThread = getThread;
|
|
95933
|
+
exports2.extractReplyText = extractReplyText;
|
|
95934
|
+
exports2.runReplyAgent = runReplyAgent;
|
|
95935
|
+
exports2.sendDraft = sendDraft;
|
|
95936
|
+
exports2.dismissMessage = dismissMessage;
|
|
95937
|
+
exports2.resetForAgent = resetForAgent;
|
|
95938
|
+
exports2.sendFollowUps = sendFollowUps;
|
|
95939
|
+
var db_1 = require_db();
|
|
95940
|
+
var email_1 = require_email();
|
|
95941
|
+
var llm_1 = require_llm();
|
|
95942
|
+
var followup_1 = require_followup();
|
|
95943
|
+
var agency_1 = require_agency();
|
|
95944
|
+
var guardrails_1 = require_guardrails();
|
|
95945
|
+
var AUTO_SENDABLE = ["interested", "price_question", "question"];
|
|
95946
|
+
function recordOutgoing(args) {
|
|
95947
|
+
(0, db_1.getDb)().run(`INSERT OR IGNORE INTO messages
|
|
95948
|
+
(lead_id, direction, from_addr, to_addr, subject, body, message_id, in_reply_to, handled, sent_by)
|
|
95949
|
+
VALUES (?, 'out', ?, ?, ?, ?, ?, ?, 'done', ?)`, [args.leadId, args.from ?? null, args.to, args.subject, args.body, args.messageId ?? null, args.inReplyTo ?? null, args.sentBy]);
|
|
95950
|
+
if (args.messageId) {
|
|
95951
|
+
(0, db_1.getDb)().run("UPDATE leads SET last_email_message_id = ? WHERE id = ?", [args.messageId, args.leadId]);
|
|
95952
|
+
}
|
|
95953
|
+
}
|
|
95954
|
+
function recordIncoming(args) {
|
|
95955
|
+
const db2 = (0, db_1.getDb)();
|
|
95956
|
+
if (args.messageId && db2.get("SELECT 1 FROM messages WHERE message_id = ?", [args.messageId]))
|
|
95957
|
+
return false;
|
|
95958
|
+
db2.run(`INSERT INTO messages (lead_id, direction, from_addr, subject, body, message_id, in_reply_to, at)
|
|
95959
|
+
VALUES (?, 'in', ?, ?, ?, ?, ?, ?)`, [
|
|
95960
|
+
args.leadId,
|
|
95961
|
+
args.from,
|
|
95962
|
+
args.subject,
|
|
95963
|
+
args.body,
|
|
95964
|
+
args.messageId,
|
|
95965
|
+
args.inReplyTo,
|
|
95966
|
+
(args.at ?? /* @__PURE__ */ new Date()).toISOString()
|
|
95967
|
+
]);
|
|
95968
|
+
return true;
|
|
95969
|
+
}
|
|
95970
|
+
function listThreads() {
|
|
95971
|
+
const rows = (0, db_1.getDb)().all(`SELECT l.id AS lead_id, l.name, l.email, l.status, l.site_url,
|
|
95972
|
+
m.at AS last_at, m.direction AS last_direction, substr(m.body, 1, 160) AS last_preview,
|
|
95973
|
+
(SELECT COUNT(*) FROM messages p WHERE p.lead_id = l.id AND p.direction = 'in'
|
|
95974
|
+
AND p.handled IN ('new','drafted','needs_you')) AS pending,
|
|
95975
|
+
(SELECT intent FROM messages q WHERE q.lead_id = l.id AND q.direction = 'in'
|
|
95976
|
+
AND q.intent IS NOT NULL ORDER BY q.at DESC LIMIT 1) AS latest_intent
|
|
95977
|
+
FROM leads l
|
|
95978
|
+
JOIN messages m ON m.id = (SELECT id FROM messages WHERE lead_id = l.id ORDER BY at DESC, id DESC LIMIT 1)
|
|
95979
|
+
ORDER BY pending > 0 DESC, m.at DESC`, []);
|
|
95980
|
+
return rows.map((r) => ({ ...r, suppressed: (0, guardrails_1.isSuppressed)(r.email) }));
|
|
95981
|
+
}
|
|
95982
|
+
function getThread(leadId) {
|
|
95983
|
+
return (0, db_1.getDb)().all("SELECT * FROM messages WHERE lead_id = ? ORDER BY at ASC, id ASC", [leadId]);
|
|
95984
|
+
}
|
|
95985
|
+
function splitHead(raw) {
|
|
95986
|
+
const m = raw.match(/\r?\n\r?\n/);
|
|
95987
|
+
if (!m || m.index === void 0)
|
|
95988
|
+
return [raw, ""];
|
|
95989
|
+
return [raw.slice(0, m.index), raw.slice(m.index + m[0].length)];
|
|
95990
|
+
}
|
|
95991
|
+
function header(head, name) {
|
|
95992
|
+
const unfolded = head.replace(/\r?\n[ \t]+/g, " ");
|
|
95993
|
+
const line = unfolded.split(/\r?\n/).find((l) => l.toLowerCase().startsWith(name.toLowerCase() + ":"));
|
|
95994
|
+
return line ? line.slice(line.indexOf(":") + 1).trim() : "";
|
|
95995
|
+
}
|
|
95996
|
+
function decodeBody(body, encoding) {
|
|
95997
|
+
const enc = encoding.toLowerCase();
|
|
95998
|
+
if (enc === "base64") {
|
|
95999
|
+
try {
|
|
96000
|
+
return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf-8");
|
|
96001
|
+
} catch {
|
|
96002
|
+
return body;
|
|
96003
|
+
}
|
|
96004
|
+
}
|
|
96005
|
+
if (enc === "quoted-printable") {
|
|
96006
|
+
const bytes = body.replace(/=\r?\n/g, "").replace(/=([0-9A-F]{2})/gi, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
96007
|
+
return Buffer.from(bytes, "latin1").toString("utf-8");
|
|
96008
|
+
}
|
|
96009
|
+
return body;
|
|
96010
|
+
}
|
|
96011
|
+
function htmlToText(html) {
|
|
96012
|
+
return html.replace(/<(style|script)[\s\S]*?<\/\1>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h\d)>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'|'/g, "'").replace(/"/g, '"');
|
|
96013
|
+
}
|
|
96014
|
+
function partText(raw, depth = 0) {
|
|
96015
|
+
const [head, body] = splitHead(raw);
|
|
96016
|
+
const type = header(head, "content-type").toLowerCase();
|
|
96017
|
+
const enc = header(head, "content-transfer-encoding");
|
|
96018
|
+
if (type.startsWith("multipart/") && depth < 5) {
|
|
96019
|
+
const b = header(head, "content-type").match(/boundary="?([^";]+)"?/i)?.[1];
|
|
96020
|
+
if (!b)
|
|
96021
|
+
return { plain: body };
|
|
96022
|
+
const out = {};
|
|
96023
|
+
for (const part of body.split(`--${b}`).slice(1)) {
|
|
96024
|
+
if (part.startsWith("--"))
|
|
96025
|
+
break;
|
|
96026
|
+
const r = partText(part.replace(/^\r?\n/, ""), depth + 1);
|
|
96027
|
+
out.plain ??= r.plain;
|
|
96028
|
+
out.html ??= r.html;
|
|
96029
|
+
}
|
|
96030
|
+
return out;
|
|
96031
|
+
}
|
|
96032
|
+
if (type.includes("text/html"))
|
|
96033
|
+
return { html: decodeBody(body, enc) };
|
|
96034
|
+
if (!type || type.includes("text/plain"))
|
|
96035
|
+
return { plain: decodeBody(body, enc) };
|
|
96036
|
+
return {};
|
|
96037
|
+
}
|
|
96038
|
+
function extractReplyText(raw) {
|
|
96039
|
+
const parts = partText(raw);
|
|
96040
|
+
const text = parts.plain ?? (parts.html ? htmlToText(parts.html) : "");
|
|
96041
|
+
const lines = [];
|
|
96042
|
+
for (const line of text.split(/\r?\n/)) {
|
|
96043
|
+
const t = line.trim();
|
|
96044
|
+
if (t === "--" || t === "-- ")
|
|
96045
|
+
break;
|
|
96046
|
+
if (/^On .+wrote:$/i.test(t) || /^-{2,} ?Original Message ?-{2,}$/i.test(t) || /^From: .+/.test(t) && lines.length > 0)
|
|
96047
|
+
break;
|
|
96048
|
+
if (t.startsWith(">"))
|
|
96049
|
+
continue;
|
|
96050
|
+
lines.push(line.trimEnd());
|
|
96051
|
+
}
|
|
96052
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim().slice(0, 4e3);
|
|
96053
|
+
}
|
|
96054
|
+
function parseJson(text) {
|
|
96055
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
96056
|
+
if (!m)
|
|
96057
|
+
return null;
|
|
96058
|
+
try {
|
|
96059
|
+
const j = JSON.parse(m[0]);
|
|
96060
|
+
const intents = ["interested", "price_question", "question", "change_request", "not_interested", "unsubscribe", "auto_reply", "other"];
|
|
96061
|
+
return {
|
|
96062
|
+
intent: intents.includes(j.intent) ? j.intent : "other",
|
|
96063
|
+
confidence: Math.max(0, Math.min(1, Number(j.confidence) || 0)),
|
|
96064
|
+
summary: String(j.summary ?? "").slice(0, 300),
|
|
96065
|
+
reply_subject: j.reply_subject ? String(j.reply_subject) : null,
|
|
96066
|
+
reply_body: j.reply_body ? String(j.reply_body) : null
|
|
96067
|
+
};
|
|
96068
|
+
} catch {
|
|
96069
|
+
return null;
|
|
96070
|
+
}
|
|
96071
|
+
}
|
|
96072
|
+
async function analyse(msg, deps) {
|
|
96073
|
+
const db2 = (0, db_1.getDb)();
|
|
96074
|
+
const lead = db2.get("SELECT name, type, city, status, site_url, booking_url, handed_over_at FROM leads WHERE id = ?", [msg.lead_id]);
|
|
96075
|
+
if (!lead)
|
|
96076
|
+
return null;
|
|
96077
|
+
const s = (0, guardrails_1.getAutopilotSettings)();
|
|
96078
|
+
const agency2 = (0, agency_1.getAgencyProfile)();
|
|
96079
|
+
const history = getThread(msg.lead_id).filter((m) => m.id !== msg.id).slice(-6).map((m) => `${m.direction === "out" ? "US" : "THEM"} (${m.at.slice(0, 10)}): ${(m.body ?? "").slice(0, 1200)}`).join("\n\n");
|
|
96080
|
+
const isClient = !!lead.handed_over_at || lead.status === "won";
|
|
96081
|
+
const prompt = `You handle email replies for ${agency2.name ?? "a small web design studio"}, which builds websites for local businesses.
|
|
96082
|
+
|
|
96083
|
+
Business you're talking to: ${lead.name}${lead.type ? ` (${lead.type})` : ""}${lead.city ? `, ${lead.city}` : ""}.
|
|
96084
|
+
${isClient ? "They are already a paying client." : `They're a prospect. We built them a free demo site${lead.site_url ? `: ${lead.site_url}` : ""}.`}
|
|
96085
|
+
|
|
96086
|
+
Pricing you may quote (never discount or promise anything else):
|
|
96087
|
+
- Website made live on their own domain: ${(0, agency_1.formatMoney)(s.sitePence, agency2.currency)} one-off.
|
|
96088
|
+
${s.carePence > 0 ? `- Optional care plan (hosting, updates, small edits): ${(0, agency_1.formatMoney)(s.carePence, agency2.currency)} a month.` : "- Hosting is included for the first year."}
|
|
96089
|
+
${s.callUrl ? `Call booking link: ${s.callUrl}` : "No call booking link \u2014 offer to reply by email or ask for a good time to call."}
|
|
96090
|
+
|
|
96091
|
+
Earlier emails:
|
|
96092
|
+
${history || "(none stored)"}
|
|
96093
|
+
|
|
96094
|
+
Their new email:
|
|
96095
|
+
${(msg.body ?? "").slice(0, 3e3)}
|
|
96096
|
+
|
|
96097
|
+
Classify the new email and, unless it's an unsubscribe, an auto-reply or a clear no, write the reply we should send.
|
|
96098
|
+
|
|
96099
|
+
Intents: interested (wants to go ahead or keen), price_question, question (anything else they ask), change_request (wants changes to the site), not_interested, unsubscribe (asks not to be contacted), auto_reply (out-of-office or automated), other.
|
|
96100
|
+
|
|
96101
|
+
Reply rules:
|
|
96102
|
+
- Plain, friendly, professional British English. Short: 60\u2013140 words. No hype, no pressure.
|
|
96103
|
+
- Answer exactly what they asked. If you don't know something (dates, technical specifics not given above), say we'll confirm rather than inventing it.
|
|
96104
|
+
- If they're interested or asking the price and want to proceed, include the literal placeholder {payment_link} on its own line as the way to go ahead.
|
|
96105
|
+
- Use {call_link} only if a call booking link exists above.
|
|
96106
|
+
- Sign off as ${agency2.name ?? "the team"}.
|
|
96107
|
+
- Never claim results, guarantees or testimonials.
|
|
96108
|
+
|
|
96109
|
+
Respond with JSON only:
|
|
96110
|
+
{"intent": "...", "confidence": 0.0-1.0, "summary": "one sentence on what they want", "reply_subject": "Re: ..." or null, "reply_body": "..." or null}`;
|
|
96111
|
+
const res = await (0, llm_1.generateText)({ apiKey: deps.apiKey, prompt, maxTokens: 1500, timeoutMs: 12e4 });
|
|
96112
|
+
return parseJson(res.text);
|
|
96113
|
+
}
|
|
96114
|
+
function fill(body, links) {
|
|
96115
|
+
let out = body;
|
|
96116
|
+
out = links.payment ? out.replace(/\{payment_link\}/g, links.payment) : out.split("\n").filter((l) => !l.includes("{payment_link}")).join("\n");
|
|
96117
|
+
out = links.call ? out.replace(/\{call_link\}/g, links.call) : out.split("\n").filter((l) => !l.includes("{call_link}")).join("\n");
|
|
96118
|
+
return out.trim();
|
|
96119
|
+
}
|
|
96120
|
+
async function runReplyAgent(deps) {
|
|
96121
|
+
const settings = (0, guardrails_1.getAutopilotSettings)();
|
|
96122
|
+
if (settings.replyMode === "off")
|
|
96123
|
+
return { processed: 0, sent: 0 };
|
|
96124
|
+
const db2 = (0, db_1.getDb)();
|
|
96125
|
+
const pending = db2.all("SELECT * FROM messages WHERE direction = 'in' AND handled = 'new' ORDER BY at ASC LIMIT 10", []);
|
|
96126
|
+
let processed = 0;
|
|
96127
|
+
let sent = 0;
|
|
96128
|
+
for (const msg of pending) {
|
|
96129
|
+
let a = null;
|
|
96130
|
+
try {
|
|
96131
|
+
a = await analyse(msg, deps);
|
|
96132
|
+
} catch (e) {
|
|
96133
|
+
(0, guardrails_1.logActivity)({ action: "Couldn't read reply", leadId: msg.lead_id, detail: e.message, needsYou: true });
|
|
96134
|
+
}
|
|
96135
|
+
if (!a) {
|
|
96136
|
+
db2.run("UPDATE messages SET handled = 'needs_you' WHERE id = ?", [msg.id]);
|
|
96137
|
+
continue;
|
|
96138
|
+
}
|
|
96139
|
+
processed++;
|
|
96140
|
+
db2.run("UPDATE messages SET intent = ?, intent_summary = ?, confidence = ? WHERE id = ?", [a.intent, a.summary, a.confidence, msg.id]);
|
|
96141
|
+
if (a.intent === "unsubscribe" || a.intent === "not_interested") {
|
|
96142
|
+
if (msg.from_addr)
|
|
96143
|
+
(0, guardrails_1.suppress)(msg.from_addr, a.intent === "unsubscribe" ? "unsubscribed" : "not interested", msg.lead_id);
|
|
96144
|
+
(0, followup_1.logContact)({ leadId: msg.lead_id, direction: "in", channel: "email", outcome: "not_interested", note: a.summary });
|
|
96145
|
+
db2.run("UPDATE messages SET handled = 'done' WHERE id = ?", [msg.id]);
|
|
96146
|
+
(0, guardrails_1.logActivity)({ action: a.intent === "unsubscribe" ? "Unsubscribed \u2014 won't be contacted again" : "Not interested \u2014 closed", leadId: msg.lead_id, detail: a.summary });
|
|
96147
|
+
continue;
|
|
96148
|
+
}
|
|
96149
|
+
if (a.intent === "auto_reply") {
|
|
96150
|
+
db2.run("UPDATE messages SET handled = 'ignored' WHERE id = ?", [msg.id]);
|
|
96151
|
+
continue;
|
|
96152
|
+
}
|
|
96153
|
+
if (a.intent === "interested") {
|
|
96154
|
+
(0, followup_1.logContact)({ leadId: msg.lead_id, direction: "in", channel: "email", outcome: "interested", note: a.summary });
|
|
96155
|
+
}
|
|
96156
|
+
let body = a.reply_body ?? "";
|
|
96157
|
+
let payment = null;
|
|
96158
|
+
if (body.includes("{payment_link}") && settings.sendProposals) {
|
|
96159
|
+
try {
|
|
96160
|
+
payment = await deps.createPaymentLink(msg.lead_id);
|
|
96161
|
+
} catch (e) {
|
|
96162
|
+
(0, guardrails_1.logActivity)({ action: "Couldn't create payment link", leadId: msg.lead_id, detail: e.message, needsYou: true });
|
|
96163
|
+
}
|
|
96164
|
+
}
|
|
96165
|
+
body = fill(body, { payment, call: settings.callUrl });
|
|
96166
|
+
const subject = a.reply_subject || `Re: ${msg.subject ?? ""}`.trim();
|
|
96167
|
+
db2.run("UPDATE messages SET draft_subject = ?, draft_body = ?, handled = 'drafted' WHERE id = ?", [subject, body, msg.id]);
|
|
96168
|
+
const canAuto = settings.replyMode === "auto" && AUTO_SENDABLE.includes(a.intent) && a.confidence >= settings.minConfidence && !!body && !(0, guardrails_1.isSuppressed)(msg.from_addr) && !(0, guardrails_1.sendBlockedReason)();
|
|
96169
|
+
if (canAuto) {
|
|
96170
|
+
const r = await sendDraft(msg.id, { subject, body }, deps.smtp, "autopilot");
|
|
96171
|
+
if (r.ok) {
|
|
96172
|
+
sent++;
|
|
96173
|
+
(0, guardrails_1.logActivity)({ action: `Replied automatically (${a.intent.replace("_", " ")})`, leadId: msg.lead_id, detail: a.summary });
|
|
96174
|
+
continue;
|
|
96175
|
+
}
|
|
96176
|
+
(0, guardrails_1.logActivity)({ action: "Auto-reply failed to send", leadId: msg.lead_id, detail: r.error, needsYou: true });
|
|
96177
|
+
}
|
|
96178
|
+
const why = settings.replyMode === "draft" ? "Draft ready for you to check" : !AUTO_SENDABLE.includes(a.intent) ? `Needs you (${a.intent.replace("_", " ")})` : a.confidence < settings.minConfidence ? "Needs you (agent wasn't sure)" : (0, guardrails_1.sendBlockedReason)() ?? "Needs you";
|
|
96179
|
+
(0, guardrails_1.logActivity)({ action: why, leadId: msg.lead_id, detail: a.summary, needsYou: true });
|
|
96180
|
+
}
|
|
96181
|
+
if (processed)
|
|
96182
|
+
deps.emit?.("inbox:changed", { processed, sent });
|
|
96183
|
+
return { processed, sent };
|
|
96184
|
+
}
|
|
96185
|
+
async function sendDraft(messageId, content, smtp, sentBy) {
|
|
96186
|
+
if (!smtp)
|
|
96187
|
+
return { ok: false, error: "Set up email sending in Settings first." };
|
|
96188
|
+
const db2 = (0, db_1.getDb)();
|
|
96189
|
+
const msg = db2.get("SELECT * FROM messages WHERE id = ?", [messageId]);
|
|
96190
|
+
if (!msg)
|
|
96191
|
+
return { ok: false, error: "Message not found." };
|
|
96192
|
+
if (!msg.from_addr)
|
|
96193
|
+
return { ok: false, error: "No address to reply to." };
|
|
96194
|
+
if ((0, guardrails_1.isSuppressed)(msg.from_addr))
|
|
96195
|
+
return { ok: false, error: "This address is on the do-not-contact list." };
|
|
96196
|
+
if (sentBy === "autopilot") {
|
|
96197
|
+
const blocked = (0, guardrails_1.sendBlockedReason)();
|
|
96198
|
+
if (blocked)
|
|
96199
|
+
return { ok: false, error: blocked };
|
|
96200
|
+
}
|
|
96201
|
+
const res = await (0, email_1.sendEmail)(smtp, {
|
|
96202
|
+
to: msg.from_addr,
|
|
96203
|
+
subject: content.subject,
|
|
96204
|
+
text: content.body,
|
|
96205
|
+
inReplyTo: msg.message_id,
|
|
96206
|
+
references: msg.message_id
|
|
96207
|
+
});
|
|
96208
|
+
if (!res.ok)
|
|
96209
|
+
return { ok: false, error: res.error };
|
|
96210
|
+
recordOutgoing({
|
|
96211
|
+
leadId: msg.lead_id,
|
|
96212
|
+
to: msg.from_addr,
|
|
96213
|
+
from: smtp.from,
|
|
96214
|
+
subject: content.subject,
|
|
96215
|
+
body: content.body,
|
|
96216
|
+
messageId: res.messageId,
|
|
96217
|
+
inReplyTo: msg.message_id,
|
|
96218
|
+
sentBy
|
|
96219
|
+
});
|
|
96220
|
+
db2.run("UPDATE messages SET handled = 'replied', draft_subject = ?, draft_body = ? WHERE id = ?", [content.subject, content.body, messageId]);
|
|
96221
|
+
(0, followup_1.logContact)({ leadId: msg.lead_id, direction: "out", channel: "email", note: content.subject });
|
|
96222
|
+
return { ok: true };
|
|
96223
|
+
}
|
|
96224
|
+
function dismissMessage(messageId) {
|
|
96225
|
+
(0, db_1.getDb)().run("UPDATE messages SET handled = 'done' WHERE id = ?", [messageId]);
|
|
96226
|
+
}
|
|
96227
|
+
function resetForAgent(messageId) {
|
|
96228
|
+
(0, db_1.getDb)().run("UPDATE messages SET handled = 'new', draft_subject = NULL, draft_body = NULL WHERE id = ? AND direction = 'in'", [messageId]);
|
|
96229
|
+
}
|
|
96230
|
+
async function sendFollowUps(args) {
|
|
96231
|
+
if (!args.smtp)
|
|
96232
|
+
return { sent: 0, skipped: 0, error: "Email sending isn't set up." };
|
|
96233
|
+
const db2 = (0, db_1.getDb)();
|
|
96234
|
+
const due = db2.all(`SELECT id, name, email, site_url, booking_url, last_email_message_id
|
|
96235
|
+
FROM leads
|
|
96236
|
+
WHERE last_contacted_at IS NOT NULL
|
|
96237
|
+
AND last_reply_at IS NULL AND replied_at IS NULL
|
|
96238
|
+
AND status NOT IN ('dead','won')
|
|
96239
|
+
AND COALESCE(followed_up_count, 0) <= 1
|
|
96240
|
+
AND email IS NOT NULL AND email != ''
|
|
96241
|
+
AND julianday('now') - julianday(last_contacted_at) >= ?
|
|
96242
|
+
ORDER BY last_contacted_at ASC
|
|
96243
|
+
LIMIT ?`, [args.afterDays, args.limit]);
|
|
96244
|
+
const agency2 = (0, agency_1.getAgencyProfile)().name ?? "";
|
|
96245
|
+
let sent = 0;
|
|
96246
|
+
let skipped = 0;
|
|
96247
|
+
for (const l of due) {
|
|
96248
|
+
if ((0, guardrails_1.isSuppressed)(l.email)) {
|
|
96249
|
+
skipped++;
|
|
96250
|
+
continue;
|
|
96251
|
+
}
|
|
96252
|
+
const blocked = (0, guardrails_1.sendBlockedReason)();
|
|
96253
|
+
if (blocked)
|
|
96254
|
+
return { sent, skipped: skipped + (due.length - sent - skipped), error: blocked };
|
|
96255
|
+
const link = l.site_url || l.booking_url || "";
|
|
96256
|
+
const subject = `Following up \u2014 ${l.name}`;
|
|
96257
|
+
const body = [
|
|
96258
|
+
"Hello,",
|
|
96259
|
+
"",
|
|
96260
|
+
`Just following up on the free demo website I put together for ${l.name}${link ? `:
|
|
96261
|
+
${link}` : "."}`,
|
|
96262
|
+
"",
|
|
96263
|
+
`Happy to answer any questions or make changes. If it's not for you, just reply "no thanks" and I won't get in touch again.`,
|
|
96264
|
+
"",
|
|
96265
|
+
"Best regards",
|
|
96266
|
+
agency2
|
|
96267
|
+
].join("\n");
|
|
96268
|
+
const res = await (0, email_1.sendEmail)(args.smtp, {
|
|
96269
|
+
to: l.email,
|
|
96270
|
+
subject,
|
|
96271
|
+
text: body,
|
|
96272
|
+
inReplyTo: l.last_email_message_id,
|
|
96273
|
+
references: l.last_email_message_id
|
|
96274
|
+
});
|
|
96275
|
+
if (!res.ok) {
|
|
96276
|
+
(0, guardrails_1.logActivity)({ action: "Follow-up failed to send", leadId: l.id, detail: res.error });
|
|
96277
|
+
skipped++;
|
|
96278
|
+
continue;
|
|
96279
|
+
}
|
|
96280
|
+
recordOutgoing({ leadId: l.id, to: l.email, from: args.smtp.from, subject, body, messageId: res.messageId ?? null, inReplyTo: l.last_email_message_id, sentBy: "autopilot" });
|
|
96281
|
+
(0, followup_1.logContact)({ leadId: l.id, direction: "out", channel: "email", note: "follow-up" });
|
|
96282
|
+
(0, guardrails_1.logActivity)({ action: "Sent a follow-up", leadId: l.id });
|
|
96283
|
+
sent++;
|
|
96284
|
+
}
|
|
96285
|
+
return { sent, skipped };
|
|
96286
|
+
}
|
|
96287
|
+
}
|
|
96288
|
+
});
|
|
96289
|
+
|
|
95709
96290
|
// ../desktop/dist/main/lib/chat-tools.js
|
|
95710
96291
|
var require_chat_tools = __commonJS({
|
|
95711
96292
|
"../desktop/dist/main/lib/chat-tools.js"(exports2) {
|
|
@@ -95727,6 +96308,8 @@ var require_chat_tools = __commonJS({
|
|
|
95727
96308
|
var site_stats_1 = require_site_stats();
|
|
95728
96309
|
var email_1 = require_email();
|
|
95729
96310
|
var email_harvest_1 = require_email_harvest();
|
|
96311
|
+
var guardrails_1 = require_guardrails();
|
|
96312
|
+
var inbox_1 = require_inbox();
|
|
95730
96313
|
exports2.TOOLS = [
|
|
95731
96314
|
{
|
|
95732
96315
|
name: "find_leads",
|
|
@@ -95806,7 +96389,7 @@ var require_chat_tools = __commonJS({
|
|
|
95806
96389
|
},
|
|
95807
96390
|
{
|
|
95808
96391
|
name: "whatsapp_message",
|
|
95809
|
-
description: "Generate a pre-filled WhatsApp message link for a lead. Returns a URL the user can open. Doesn't actually send.",
|
|
96392
|
+
description: "Generate a pre-filled WhatsApp message link for a lead. Only use this when the user explicitly asks for WhatsApp \u2014 outreach is email by default. Returns a URL the user can open. Doesn't actually send.",
|
|
95810
96393
|
input_schema: {
|
|
95811
96394
|
type: "object",
|
|
95812
96395
|
properties: {
|
|
@@ -95833,7 +96416,7 @@ var require_chat_tools = __commonJS({
|
|
|
95833
96416
|
channel: {
|
|
95834
96417
|
type: "string",
|
|
95835
96418
|
enum: ["email", "whatsapp", "sms"],
|
|
95836
|
-
description: "Preferred channel. 'email' (default) sends via the user's SMTP \u2014 the only channel that works unattended on every platform
|
|
96419
|
+
description: "Preferred channel. 'email' (default, and the professional choice) sends via the user's SMTP \u2014 the only channel that works unattended on every platform. Leads with no email address are reported as unreachable, never switched to WhatsApp. 'whatsapp' opens WhatsApp and 'sms' opens the device's Messages app (Messages on macOS, Phone Link on Windows); both fall back to email when a lead has no phone. Omit to use the saved preference."
|
|
95837
96420
|
}
|
|
95838
96421
|
}
|
|
95839
96422
|
},
|
|
@@ -96202,11 +96785,19 @@ Want it live on your own domain?`;
|
|
|
96202
96785
|
}
|
|
96203
96786
|
function outreachTemplate(db2) {
|
|
96204
96787
|
const row = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_template"]);
|
|
96205
|
-
return row?.value ?? `
|
|
96788
|
+
return row?.value ?? `Hello,
|
|
96789
|
+
|
|
96790
|
+
I came across {name} on Google and noticed you don't have a website yet, so I put together a free demo to show what one could look like:
|
|
96791
|
+
|
|
96792
|
+
{site_url}
|
|
96793
|
+
|
|
96794
|
+
It's built from your real reviews and photos. If you like it, I can put it live on your own domain with your details for a one-off fee of \xA3199 \u2014 no monthly charges.
|
|
96206
96795
|
|
|
96207
|
-
|
|
96796
|
+
Would you be open to a quick chat this week?
|
|
96208
96797
|
|
|
96209
|
-
|
|
96798
|
+
Best regards
|
|
96799
|
+
|
|
96800
|
+
If you'd rather not hear from me again, just reply and let me know.`;
|
|
96210
96801
|
}
|
|
96211
96802
|
function normaliseChannel(value) {
|
|
96212
96803
|
switch (String(value ?? "").toLowerCase()) {
|
|
@@ -96352,11 +96943,19 @@ Interested?`;
|
|
|
96352
96943
|
const phone = (lead.phone ?? "").replace(/[^\d+]/g, "");
|
|
96353
96944
|
const e164 = phone.startsWith("+") ? phone : phone.startsWith("0") ? `+44${phone.slice(1)}` : phone;
|
|
96354
96945
|
const tplRow = db2.get("SELECT value FROM settings WHERE key = ?", ["outreach_template"]);
|
|
96355
|
-
const template = tplRow?.value ?? `
|
|
96946
|
+
const template = tplRow?.value ?? `Hello,
|
|
96947
|
+
|
|
96948
|
+
I came across {name} on Google and noticed you don't have a website yet, so I put together a free demo to show what one could look like:
|
|
96949
|
+
|
|
96950
|
+
{site_url}
|
|
96951
|
+
|
|
96952
|
+
It's built from your real reviews and photos. If you like it, I can put it live on your own domain with your details for a one-off fee of \xA3199 \u2014 no monthly charges.
|
|
96953
|
+
|
|
96954
|
+
Would you be open to a quick chat this week?
|
|
96356
96955
|
|
|
96357
|
-
|
|
96956
|
+
Best regards
|
|
96358
96957
|
|
|
96359
|
-
|
|
96958
|
+
If you'd rather not hear from me again, just reply and let me know.`;
|
|
96360
96959
|
const body = input.message ?? template.replace(/\{name\}/g, lead.name).replace(/\{site_url\}/g, lead.site_url ?? "");
|
|
96361
96960
|
const url = `https://wa.me/${e164.replace(/^\+/, "")}?text=${encodeURIComponent(body)}`;
|
|
96362
96961
|
return { url, message: body, phone: e164 };
|
|
@@ -96383,7 +96982,7 @@ Interested?`;
|
|
|
96383
96982
|
const template = outreachTemplate(db2);
|
|
96384
96983
|
const bookingTemplate = bookingOutreachTemplate(db2);
|
|
96385
96984
|
const preferEmail = channel === "email";
|
|
96386
|
-
const linkChannel = channel === "email" ?
|
|
96985
|
+
const linkChannel = channel === "email" ? null : channel;
|
|
96387
96986
|
const smtp = (0, email_1.smtpFromSettings)((k) => getDecryptedSetting(k));
|
|
96388
96987
|
const emailSubject = getDecryptedSetting("outreach_email_subject") || "A free demo website for {name}";
|
|
96389
96988
|
const prepared = [];
|
|
@@ -96409,9 +97008,17 @@ Interested?`;
|
|
|
96409
97008
|
harvested++;
|
|
96410
97009
|
}
|
|
96411
97010
|
}
|
|
97011
|
+
if (r.email && (0, guardrails_1.isSuppressed)(r.email)) {
|
|
97012
|
+
prepared.push({ id: r.id, name: r.name, via: "none", email: r.email, message, emailError: "On the do-not-contact list" });
|
|
97013
|
+
continue;
|
|
97014
|
+
}
|
|
97015
|
+
if (preferEmail && (0, guardrails_1.emailsSentToday)() >= (0, guardrails_1.getAutopilotSettings)().maxEmailsPerDay) {
|
|
97016
|
+
prepared.push({ id: r.id, name: r.name, via: "none", email: r.email ?? void 0, message, emailError: "Daily email limit reached \u2014 try again tomorrow" });
|
|
97017
|
+
continue;
|
|
97018
|
+
}
|
|
96412
97019
|
const leadEmail = smtp && r.email ? r.email : null;
|
|
96413
97020
|
const useEmail = leadEmail !== null && (preferEmail || !e164);
|
|
96414
|
-
if (!useEmail && e164) {
|
|
97021
|
+
if (!useEmail && e164 && linkChannel) {
|
|
96415
97022
|
prepared.push({
|
|
96416
97023
|
id: r.id,
|
|
96417
97024
|
name: r.name,
|
|
@@ -96423,11 +97030,16 @@ Interested?`;
|
|
|
96423
97030
|
} else if (useEmail && smtp && leadEmail) {
|
|
96424
97031
|
const subject = emailSubject.replace(/\{name\}/g, r.name);
|
|
96425
97032
|
const res = await (0, email_1.sendEmail)(smtp, { to: leadEmail, subject, text: message });
|
|
96426
|
-
if (res.ok
|
|
96427
|
-
|
|
96428
|
-
|
|
96429
|
-
|
|
96430
|
-
|
|
97033
|
+
if (res.ok) {
|
|
97034
|
+
(0, inbox_1.recordOutgoing)({
|
|
97035
|
+
leadId: r.id,
|
|
97036
|
+
to: leadEmail,
|
|
97037
|
+
from: smtp.from,
|
|
97038
|
+
subject,
|
|
97039
|
+
body: message,
|
|
97040
|
+
messageId: res.messageId ?? null,
|
|
97041
|
+
sentBy: "outreach"
|
|
97042
|
+
});
|
|
96431
97043
|
}
|
|
96432
97044
|
prepared.push({
|
|
96433
97045
|
id: r.id,
|
|
@@ -96457,7 +97069,7 @@ Interested?`;
|
|
|
96457
97069
|
(0, followup_1.logContact)({
|
|
96458
97070
|
leadId: r.id,
|
|
96459
97071
|
direction: "out",
|
|
96460
|
-
channel: last.via === "email" ? "email" : linkChannel
|
|
97072
|
+
channel: last.via === "email" ? "email" : linkChannel ?? "sms"
|
|
96461
97073
|
});
|
|
96462
97074
|
}
|
|
96463
97075
|
}
|
|
@@ -96890,7 +97502,7 @@ var require_chat = __commonJS({
|
|
|
96890
97502
|
var sdk_1 = __importDefault(require_sdk());
|
|
96891
97503
|
var chat_tools_1 = require_chat_tools();
|
|
96892
97504
|
var models_1 = require_models();
|
|
96893
|
-
var SYSTEM_PROMPT = `You are Sproutkit's built-in assistant \u2014 STRICTLY scoped to helping the user run their lead-generation, website-building, and outreach work inside the Sproutkit app.
|
|
97505
|
+
var SYSTEM_PROMPT = `You are Sprout, Sproutkit's built-in assistant (the operator's friendly sidekick) \u2014 STRICTLY scoped to helping the user run their lead-generation, website-building, and outreach work inside the Sproutkit app.
|
|
96894
97506
|
|
|
96895
97507
|
You can do the user's real work through tools \u2014 you are not a help desk pointing at buttons. Many users prefer chatting to using the UI, so if something can be done with a tool, DO IT.
|
|
96896
97508
|
|
|
@@ -96937,7 +97549,7 @@ Confirmation rules:
|
|
|
96937
97549
|
You can help with:
|
|
96938
97550
|
- Finding leads (running the search itself, plus strategy, categories, filtering)
|
|
96939
97551
|
- Building demo websites (design ideas, copy suggestions, fixing issues)
|
|
96940
|
-
- Outreach (
|
|
97552
|
+
- Outreach (email by default \u2014 draft and send professional emails; only use WhatsApp/SMS when the user asks; follow-ups, kanban moves)
|
|
96941
97553
|
- The CMS, claim flow, customer onboarding
|
|
96942
97554
|
- Sproutkit features and how to use them
|
|
96943
97555
|
- Pricing, business advice for selling websites to local small businesses
|
|
@@ -96946,7 +97558,7 @@ STAYING ON THE RAILS \u2014 you are capable, but you only act through the tools
|
|
|
96946
97558
|
- You can ONLY do what a tool does. If a request maps to a tool (even loosely, via an instructions field), do it. If NO tool fits \u2014 e.g. "run their Google Ads", "post to their Instagram", "phone the owner", "take payment" \u2014 say so plainly in one line and name the closest thing you CAN do. Example: "I can't run ads, but I can build them a site and draft the outreach \u2014 want that?"
|
|
96947
97559
|
- NEVER claim to have done something you have no tool for. NEVER invent a result, URL, price, or status. If you didn't get it from a tool result, don't state it as fact.
|
|
96948
97560
|
- Don't drift into general marketing consultancy, coding help, or business strategy essays. A one-line pointer is fine; a lecture is not.
|
|
96949
|
-
- Refuse anything genuinely off-topic (general coding, creative writing, personal advice, news, politics) briefly: "I'm Sproutkit's assistant \u2014 I help with finding leads, building sites, booking, and outreach. What would you like to do?" and stop.
|
|
97561
|
+
- Refuse anything genuinely off-topic (general coding, creative writing, personal advice, news, politics) briefly: "I'm Sprout, Sproutkit's assistant \u2014 I help with finding leads, building sites, booking, and outreach. What would you like to do?" and stop.
|
|
96950
97562
|
|
|
96951
97563
|
Be concise, action-oriented, and use plain English. Never apologise excessively. Never lecture.
|
|
96952
97564
|
|
|
@@ -98365,7 +98977,7 @@ var require_package2 = __commonJS({
|
|
|
98365
98977
|
"package.json"(exports2, module2) {
|
|
98366
98978
|
module2.exports = {
|
|
98367
98979
|
name: "sproutkit-code",
|
|
98368
|
-
version: "1.
|
|
98980
|
+
version: "1.10.0",
|
|
98369
98981
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
98370
98982
|
bin: {
|
|
98371
98983
|
sproutkit: "./dist/index.js"
|