@kivimedia/kmhub 2.9.0 → 2.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/README.md +170 -170
- package/bin/kmhub.mjs +896 -896
- package/coach-book-output-guard.mjs +760 -760
- package/index.mjs +57 -57
- package/package.json +56 -56
- package/prompts/briefing.md +29 -29
- package/prompts/luxury.md +70 -70
- package/prompts/play.md +49 -49
- package/prompts/run.md +36 -36
- package/prompts/setup.md +33 -33
- package/prompts/vs-booked.md +46 -46
- package/prompts/what-can-you-do.md +40 -40
- package/prompts.mjs +110 -109
- package/read-only-tools.json +143 -142
- package/remote.mjs +929 -929
- package/tools/balloon-costing.mjs +80 -80
- package/tools/booking-equipment.mjs +110 -110
- package/tools/bridges.mjs +54 -54
- package/tools/briefing.mjs +91 -91
- package/tools/calendar.mjs +170 -170
- package/tools/capabilities.mjs +155 -155
- package/tools/catalog.mjs +288 -288
- package/tools/clubs.mjs +176 -176
- package/tools/coach.mjs +771 -771
- package/tools/compare.mjs +76 -76
- package/tools/core.mjs +244 -244
- package/tools/crm.mjs +209 -209
- package/tools/dubsado.mjs +137 -137
- package/tools/exports.mjs +128 -128
- package/tools/fact-review.mjs +125 -125
- package/tools/flows.mjs +261 -261
- package/tools/forms.mjs +158 -158
- package/tools/gols.mjs +134 -134
- package/tools/hr.mjs +162 -162
- package/tools/knowledge.mjs +125 -125
- package/tools/marketing.mjs +396 -396
- package/tools/meta.mjs +245 -245
- package/tools/military.mjs +244 -244
- package/tools/money.mjs +235 -197
- package/tools/outreach.mjs +238 -238
- package/tools/pending.mjs +122 -122
- package/tools/photos.mjs +140 -140
- package/tools/plays.mjs +244 -244
- package/tools/profile.mjs +118 -118
- package/tools/radar.mjs +173 -173
- package/tools/recurring-invoices.mjs +149 -149
- package/tools/reengage.mjs +434 -434
- package/tools/schedules.mjs +55 -55
- package/tools/setup.mjs +168 -168
- package/tools/sops-bridges.mjs +86 -86
- package/tools/sops.mjs +314 -314
- package/tools/sourcing.mjs +268 -268
- package/tools/strategy.mjs +146 -146
- package/tools/studio.mjs +132 -132
- package/tools/venueradar.mjs +151 -151
- package/tools/voice.mjs +134 -134
- package/tools.mjs +407 -407
package/tools/sourcing.mjs
CHANGED
|
@@ -1,268 +1,268 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool family: sourcing - finding new people to work with.
|
|
3
|
-
*
|
|
4
|
-
* THIS IS THE ONE FAMILY THAT SPENDS THE CLIENT'S MONEY. Every other family
|
|
5
|
-
* reads or drafts for free. Three tools here reach paid outside services on KM
|
|
6
|
-
* Hub's accounts, and the whole design of this file exists to make sure a person
|
|
7
|
-
* always knows the price before a cent moves:
|
|
8
|
-
*
|
|
9
|
-
* - Every paid tool takes `confirm_spend`. Left out, the tool returns the
|
|
10
|
-
* current price and changes nothing. That first call is free, always.
|
|
11
|
-
* - The price is not written down here. It comes back live from KM Hub, which
|
|
12
|
-
* works it out from what this workspace has actually paid for the same thing.
|
|
13
|
-
* - A workspace that has run out of its monthly allowance is refused with the
|
|
14
|
-
* same sentence it would see in the browser, not quietly charged.
|
|
15
|
-
*
|
|
16
|
-
* Nothing here sends anything to anybody. A scout writes its first message as a
|
|
17
|
-
* DRAFT into the Approval Queue and a human releases it.
|
|
18
|
-
*
|
|
19
|
-
* The family contract this file follows is documented in ./README.md.
|
|
20
|
-
*/
|
|
21
|
-
import { z } from 'zod';
|
|
22
|
-
|
|
23
|
-
export const FAMILY = 'sourcing';
|
|
24
|
-
|
|
25
|
-
export const TOOLS = [
|
|
26
|
-
'km_sourcing_prices',
|
|
27
|
-
'km_start_lead_scout',
|
|
28
|
-
'km_list_lead_scouts',
|
|
29
|
-
'km_lead_scout_status',
|
|
30
|
-
'km_stop_lead_scout',
|
|
31
|
-
'km_find_company_contacts',
|
|
32
|
-
'km_check_email_address',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// Finding new people is outreach work, so this family joins that profile. It is
|
|
36
|
-
// not in `core`: a session that only wants the cheap tools should not be handed
|
|
37
|
-
// the spending ones.
|
|
38
|
-
export const PROFILES = ['outreach'];
|
|
39
|
-
|
|
40
|
-
/** The API has not shipped these routes yet. Say so kindly rather than erroring. */
|
|
41
|
-
function routeMissing(r) {
|
|
42
|
-
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const NOT_SUPPORTED =
|
|
46
|
-
'This KM Hub does not have lead sourcing switched on yet. Nothing is broken, and every other tool works as normal. Nothing was charged.';
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
50
|
-
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
51
|
-
* @param {{ out: Function, text: Function, qs: Function }} helpers
|
|
52
|
-
*/
|
|
53
|
-
export function register(server, call, { out, text, qs }) {
|
|
54
|
-
// Prices first: the free tool that makes every paid one safe to reach for.
|
|
55
|
-
|
|
56
|
-
server.tool(
|
|
57
|
-
'km_sourcing_prices',
|
|
58
|
-
'What the paid lead-finding tools cost in this workspace right now. Reading this is free and changes nothing. Reach for it whenever someone asks what finding leads costs, or before you suggest running a scout, so you can tell them the price in their own money rather than guessing. The figures come from what this workspace has actually been charged for the same work, so they get more accurate the more it is used. It will not run anything and it will not charge anything.',
|
|
59
|
-
{},
|
|
60
|
-
async () => {
|
|
61
|
-
const r = await call('GET', '/sourcing/prices');
|
|
62
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
63
|
-
return out(r);
|
|
64
|
-
},
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// The scout: the big one, and the only asynchronous tool in this family.
|
|
68
|
-
|
|
69
|
-
server.tool(
|
|
70
|
-
'km_start_lead_scout',
|
|
71
|
-
[
|
|
72
|
-
'Send KM Hub off to find new potential clients: it searches for people matching a description, digs out their real email address, checks the address is live, reads up on each business, and writes a first message for each one.',
|
|
73
|
-
'THIS COSTS REAL MONEY and it is the largest single charge in KM Hub. Never quote a price from memory: it depends on how many leads you ask for and it differs per workspace, so the figure comes back live from the first call. Never start one on your own initiative.',
|
|
74
|
-
'How to use it: call it FIRST without confirm_spend. Nothing is charged and nothing runs; you get back the current price for this workspace. Tell the person that price in plain words, wait for them to say yes, and only then call it again with confirm_spend set to true.',
|
|
75
|
-
'Describe who to find in ordinary language, the way the person said it: "wedding planners in Austin who do big corporate parties" works better than a pile of filters. Add the filters only if the person actually named them.',
|
|
76
|
-
'It does not run while you wait. It runs on KM Hub servers over several minutes to an hour, so start it, tell the person it is off and running, and check on it later with km_lead_scout_status. The session can end in between.',
|
|
77
|
-
'What it will NOT do: it never emails anybody. Contacts land in the pipeline and each first message is written as a DRAFT in the Approval Queue for a person to read and approve. It refuses to run at all if the workspace has used up its monthly allowance, and it will say so instead of spending.',
|
|
78
|
-
].join(' '),
|
|
79
|
-
{
|
|
80
|
-
describe: z
|
|
81
|
-
.string()
|
|
82
|
-
.optional()
|
|
83
|
-
.describe('Who to find, in plain English, in the person\'s own words. For example: "owners of boutique hotels in Miami that host weddings". This is the main input - prefer it over the filter lists below.'),
|
|
84
|
-
target_count: z
|
|
85
|
-
.number()
|
|
86
|
-
.int()
|
|
87
|
-
.min(1)
|
|
88
|
-
.max(100)
|
|
89
|
-
.optional()
|
|
90
|
-
.describe('How many contacts to aim for. Default 25. More costs more, and only about one person in three turns out to have a reachable email, so a bigger number also takes longer. Capped at 100 from here.'),
|
|
91
|
-
area: z.string().optional().describe('The town, city or region, if the person named one separately from the description.'),
|
|
92
|
-
titles: z.array(z.string()).optional().describe('Job titles to look for, only if the person named specific ones. For example ["Owner", "Events Manager"].'),
|
|
93
|
-
locations: z.array(z.string()).optional().describe('Cities or regions as separate entries, if the person listed several.'),
|
|
94
|
-
industries: z.array(z.string()).optional().describe('Industries to look in, if the person named them.'),
|
|
95
|
-
keywords: z.array(z.string()).optional().describe('Extra words that should show up on the person or the business.'),
|
|
96
|
-
company_sizes: z.array(z.string()).optional().describe('Company head-count bands, only if the person asked for a size. For example ["1,10", "11,50"].'),
|
|
97
|
-
seniorities: z.array(z.string()).optional().describe('Seniority levels, only if the person asked. For example ["owner", "founder", "director"].'),
|
|
98
|
-
avoid_keywords: z
|
|
99
|
-
.array(z.string())
|
|
100
|
-
.optional()
|
|
101
|
-
.describe('Words that disqualify a lead, matched whole-word against the company name, job title and snippet. For example ["ticketing", "parking", "dean"]. This narrows a search, it cannot be the only filter.'),
|
|
102
|
-
keep_keywords: z
|
|
103
|
-
.array(z.string())
|
|
104
|
-
.optional()
|
|
105
|
-
.describe('Words that rescue a lead from an avoid word, but only for the avoid words listed in rescuable_avoid_keywords. For example ["events", "homecoming", "graduation"].'),
|
|
106
|
-
rescuable_avoid_keywords: z
|
|
107
|
-
.array(z.string())
|
|
108
|
-
.optional()
|
|
109
|
-
.describe('Which avoid_keywords a keep_keyword is allowed to override. Use for avoid words that name a DEPARTMENT rather than a job, since those also catch that department\'s event desk - who is usually the buyer. For example ["director of athletics", "chancellor"]. Leave out to make every avoid word absolute.'),
|
|
110
|
-
outreach_context: z
|
|
111
|
-
.string()
|
|
112
|
-
.optional()
|
|
113
|
-
.describe('What the person is offering these leads, in a sentence, so the drafted first message is about something real. For example: "balloon installations for corporate galas in Austin, entrance arches and centrepieces".'),
|
|
114
|
-
vertical_flavor: z.string().optional().describe('The trade the workspace is in, if it needs to be forced. Normally leave this out and let KM Hub use the workspace setting.'),
|
|
115
|
-
deep_research: z
|
|
116
|
-
.boolean()
|
|
117
|
-
.optional()
|
|
118
|
-
.describe('Read up on each business before writing to it. On by default. Turning it off makes the run cheaper and faster but the first messages far more generic.'),
|
|
119
|
-
ai_copywriting: z
|
|
120
|
-
.boolean()
|
|
121
|
-
.optional()
|
|
122
|
-
.describe('Write a first message for each contact, as a draft. On by default. Turn it off to just collect verified contacts with no message written.'),
|
|
123
|
-
confirm_spend: z
|
|
124
|
-
.boolean()
|
|
125
|
-
.optional()
|
|
126
|
-
.describe('Leave this out on the first call: you get the price back and nothing is charged. Set it to true ONLY after the person has been told the price and has agreed to it.'),
|
|
127
|
-
confirm_token: z
|
|
128
|
-
.string()
|
|
129
|
-
.optional()
|
|
130
|
-
.describe(
|
|
131
|
-
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
132
|
-
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
133
|
-
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
134
|
-
+ 'arguments. A yes for one action never authorises a different one.',
|
|
135
|
-
),
|
|
136
|
-
},
|
|
137
|
-
async (args) => {
|
|
138
|
-
const r = await call('POST', '/sourcing/missions', args);
|
|
139
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
140
|
-
return out(r);
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
server.tool(
|
|
145
|
-
'km_list_lead_scouts',
|
|
146
|
-
'List the recent lead scouts in this workspace, newest first, with what stage each one is at. Free to call and changes nothing. Reach for it when someone asks whether anything is running, what happened to the search from yesterday, or how the last few went. If one is still going, follow up with km_lead_scout_status for its live progress. It does not start anything and does not charge anything.',
|
|
147
|
-
{
|
|
148
|
-
limit: z.number().int().min(1).max(50).optional().describe('How many to list. Default 10.'),
|
|
149
|
-
status: z
|
|
150
|
-
.enum(['queued', 'running', 'succeeded', 'failed', 'canceled'])
|
|
151
|
-
.optional()
|
|
152
|
-
.describe('Only show scouts in this state. Leave out to see them all.'),
|
|
153
|
-
},
|
|
154
|
-
async ({ limit, status }) => {
|
|
155
|
-
const r = await call('GET', `/sourcing/missions${qs({ limit: limit ?? 10, status })}`);
|
|
156
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
157
|
-
return out(r);
|
|
158
|
-
},
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
server.tool(
|
|
162
|
-
'km_lead_scout_status',
|
|
163
|
-
'Check on one lead scout: what it is doing right now, how many people it has found, verified and written to so far, and its final results once it is done. Free to call and changes nothing. Reach for it after starting a scout, or whenever someone asks how the search is getting on. Report it back as a plain sentence about where it has got to, not as a list of numbers. If it says failed, the error field explains why and the person can start a fresh one. It does not restart anything and does not charge anything.',
|
|
164
|
-
{
|
|
165
|
-
mission_id: z.string().describe('The id of the scout, as returned when it was started or by km_list_lead_scouts.'),
|
|
166
|
-
},
|
|
167
|
-
async ({ mission_id }) => {
|
|
168
|
-
const id = String(mission_id || '').trim();
|
|
169
|
-
if (!id) return text('I need the id of the scout to look it up. km_list_lead_scouts will show you the recent ones.');
|
|
170
|
-
const r = await call('GET', `/sourcing/missions/${encodeURIComponent(id)}`);
|
|
171
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
172
|
-
return out(r);
|
|
173
|
-
},
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
server.tool(
|
|
177
|
-
'km_stop_lead_scout',
|
|
178
|
-
'Stop a lead scout that is queued or already running. Reach for it the moment someone says stop, cancel, that is enough, or realises the search was set up wrong. Stopping is the money-saving move and is always safe: the workspace is only charged for the lookups already made, never for the work the scout did not do. Anything it already found stays in the pipeline, and any message it already wrote stays as a draft in the Approval Queue. It cannot stop a scout that has already finished, and it will say so plainly rather than pretending. It does not delete anything.',
|
|
179
|
-
{
|
|
180
|
-
mission_id: z.string().describe('The id of the scout to stop, from km_list_lead_scouts or from when it was started.'),
|
|
181
|
-
confirm_token: z
|
|
182
|
-
.string()
|
|
183
|
-
.optional()
|
|
184
|
-
.describe(
|
|
185
|
-
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
186
|
-
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
187
|
-
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
188
|
-
+ 'arguments. A yes for one action never authorises a different one.',
|
|
189
|
-
),
|
|
190
|
-
},
|
|
191
|
-
async ({ mission_id, confirm_token }) => {
|
|
192
|
-
const id = String(mission_id || '').trim();
|
|
193
|
-
if (!id) return text('I need the id of the scout to stop it. km_list_lead_scouts will show you which ones are still running.');
|
|
194
|
-
const r = await call('POST', `/sourcing/missions/${encodeURIComponent(id)}/cancel`, { confirm_token });
|
|
195
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
196
|
-
return out(r);
|
|
197
|
-
},
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
// The two small paid lookups: one company, or one address.
|
|
201
|
-
|
|
202
|
-
server.tool(
|
|
203
|
-
'km_find_company_contacts',
|
|
204
|
-
[
|
|
205
|
-
'Find the decision-makers at ONE named business and get their real email addresses. Use it when the person has a specific company in mind: a venue they want to work with, a business that just opened nearby, a lead in the pipeline with a company name but no contact.',
|
|
206
|
-
'THIS COSTS REAL MONEY, though only a small fraction of a full scout: it looks up at most five people at that one business.',
|
|
207
|
-
'How to use it: call it FIRST without confirm_spend. That call is free, changes nothing, and comes back with the price. Tell the person, get a yes, then call again with confirm_spend set to true.',
|
|
208
|
-
'For finding many people across many companies at once, do not loop this tool. Use km_start_lead_scout instead, which is built for it and cheaper per contact.',
|
|
209
|
-
'What it will NOT do: it never writes to anyone. It hands back names, titles and addresses, and nothing more. If nobody reachable turns up, nothing is charged for the empty result.',
|
|
210
|
-
].join(' '),
|
|
211
|
-
{
|
|
212
|
-
company: z.string().optional().describe('The business name, as the person said it. For example "The Grand Ballroom" or "Dazzling Balloons".'),
|
|
213
|
-
website: z.string().optional().describe('The company website, if known. Helps when the name is a common one. Either company or website is required.'),
|
|
214
|
-
location: z.string().optional().describe('Town or city, if it helps tell this business apart from others with the same name.'),
|
|
215
|
-
confirm_spend: z
|
|
216
|
-
.boolean()
|
|
217
|
-
.optional()
|
|
218
|
-
.describe('Leave this out on the first call: you get the price back and nothing is charged. Set it to true ONLY after the person has agreed to the cost.'),
|
|
219
|
-
confirm_token: z
|
|
220
|
-
.string()
|
|
221
|
-
.optional()
|
|
222
|
-
.describe(
|
|
223
|
-
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
224
|
-
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
225
|
-
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
226
|
-
+ 'arguments. A yes for one action never authorises a different one.',
|
|
227
|
-
),
|
|
228
|
-
},
|
|
229
|
-
async (args) => {
|
|
230
|
-
const r = await call('POST', '/sourcing/company-contacts', args);
|
|
231
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
232
|
-
return out(r);
|
|
233
|
-
},
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
server.tool(
|
|
237
|
-
'km_check_email_address',
|
|
238
|
-
[
|
|
239
|
-
'Check whether one email address is real and safe to write to, before anybody writes to it. Reach for it when someone reads an address off a business card or a website, when a contact in the pipeline looks doubtful, or before a message goes out to an address nobody has ever used.',
|
|
240
|
-
'This matters more than it sounds: writing to dead addresses is what gets a sender flagged as spam, and once that happens the good messages stop arriving too.',
|
|
241
|
-
'It COSTS REAL MONEY, but only a small amount for one address, and it charges nothing at all for an address that is obviously mistyped.',
|
|
242
|
-
'How to use it: call it FIRST without confirm_spend to see the price for free, then again with confirm_spend set to true. For a routine single check, telling the person the amount that came back and going ahead is usually enough.',
|
|
243
|
-
'It answers one of four ways: safe to write to, will bounce, risky, or could not tell. It also flags shared inboxes like info@ or sales@, which get far weaker replies than a named person.',
|
|
244
|
-
'What it will NOT do: it checks one address per call, it never writes to the address, and it never adds anybody to anything.',
|
|
245
|
-
].join(' '),
|
|
246
|
-
{
|
|
247
|
-
email: z.string().describe('The single email address to check.'),
|
|
248
|
-
confirm_spend: z
|
|
249
|
-
.boolean()
|
|
250
|
-
.optional()
|
|
251
|
-
.describe('Leave this out to see the price first without being charged. Set it to true to actually run the check.'),
|
|
252
|
-
confirm_token: z
|
|
253
|
-
.string()
|
|
254
|
-
.optional()
|
|
255
|
-
.describe(
|
|
256
|
-
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
257
|
-
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
258
|
-
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
259
|
-
+ 'arguments. A yes for one action never authorises a different one.',
|
|
260
|
-
),
|
|
261
|
-
},
|
|
262
|
-
async (args) => {
|
|
263
|
-
const r = await call('POST', '/sourcing/verify-email', args);
|
|
264
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
265
|
-
return out(r);
|
|
266
|
-
},
|
|
267
|
-
);
|
|
268
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: sourcing - finding new people to work with.
|
|
3
|
+
*
|
|
4
|
+
* THIS IS THE ONE FAMILY THAT SPENDS THE CLIENT'S MONEY. Every other family
|
|
5
|
+
* reads or drafts for free. Three tools here reach paid outside services on KM
|
|
6
|
+
* Hub's accounts, and the whole design of this file exists to make sure a person
|
|
7
|
+
* always knows the price before a cent moves:
|
|
8
|
+
*
|
|
9
|
+
* - Every paid tool takes `confirm_spend`. Left out, the tool returns the
|
|
10
|
+
* current price and changes nothing. That first call is free, always.
|
|
11
|
+
* - The price is not written down here. It comes back live from KM Hub, which
|
|
12
|
+
* works it out from what this workspace has actually paid for the same thing.
|
|
13
|
+
* - A workspace that has run out of its monthly allowance is refused with the
|
|
14
|
+
* same sentence it would see in the browser, not quietly charged.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here sends anything to anybody. A scout writes its first message as a
|
|
17
|
+
* DRAFT into the Approval Queue and a human releases it.
|
|
18
|
+
*
|
|
19
|
+
* The family contract this file follows is documented in ./README.md.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
|
|
23
|
+
export const FAMILY = 'sourcing';
|
|
24
|
+
|
|
25
|
+
export const TOOLS = [
|
|
26
|
+
'km_sourcing_prices',
|
|
27
|
+
'km_start_lead_scout',
|
|
28
|
+
'km_list_lead_scouts',
|
|
29
|
+
'km_lead_scout_status',
|
|
30
|
+
'km_stop_lead_scout',
|
|
31
|
+
'km_find_company_contacts',
|
|
32
|
+
'km_check_email_address',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Finding new people is outreach work, so this family joins that profile. It is
|
|
36
|
+
// not in `core`: a session that only wants the cheap tools should not be handed
|
|
37
|
+
// the spending ones.
|
|
38
|
+
export const PROFILES = ['outreach'];
|
|
39
|
+
|
|
40
|
+
/** The API has not shipped these routes yet. Say so kindly rather than erroring. */
|
|
41
|
+
function routeMissing(r) {
|
|
42
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const NOT_SUPPORTED =
|
|
46
|
+
'This KM Hub does not have lead sourcing switched on yet. Nothing is broken, and every other tool works as normal. Nothing was charged.';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
50
|
+
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
51
|
+
* @param {{ out: Function, text: Function, qs: Function }} helpers
|
|
52
|
+
*/
|
|
53
|
+
export function register(server, call, { out, text, qs }) {
|
|
54
|
+
// Prices first: the free tool that makes every paid one safe to reach for.
|
|
55
|
+
|
|
56
|
+
server.tool(
|
|
57
|
+
'km_sourcing_prices',
|
|
58
|
+
'What the paid lead-finding tools cost in this workspace right now. Reading this is free and changes nothing. Reach for it whenever someone asks what finding leads costs, or before you suggest running a scout, so you can tell them the price in their own money rather than guessing. The figures come from what this workspace has actually been charged for the same work, so they get more accurate the more it is used. It will not run anything and it will not charge anything.',
|
|
59
|
+
{},
|
|
60
|
+
async () => {
|
|
61
|
+
const r = await call('GET', '/sourcing/prices');
|
|
62
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
63
|
+
return out(r);
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// The scout: the big one, and the only asynchronous tool in this family.
|
|
68
|
+
|
|
69
|
+
server.tool(
|
|
70
|
+
'km_start_lead_scout',
|
|
71
|
+
[
|
|
72
|
+
'Send KM Hub off to find new potential clients: it searches for people matching a description, digs out their real email address, checks the address is live, reads up on each business, and writes a first message for each one.',
|
|
73
|
+
'THIS COSTS REAL MONEY and it is the largest single charge in KM Hub. Never quote a price from memory: it depends on how many leads you ask for and it differs per workspace, so the figure comes back live from the first call. Never start one on your own initiative.',
|
|
74
|
+
'How to use it: call it FIRST without confirm_spend. Nothing is charged and nothing runs; you get back the current price for this workspace. Tell the person that price in plain words, wait for them to say yes, and only then call it again with confirm_spend set to true.',
|
|
75
|
+
'Describe who to find in ordinary language, the way the person said it: "wedding planners in Austin who do big corporate parties" works better than a pile of filters. Add the filters only if the person actually named them.',
|
|
76
|
+
'It does not run while you wait. It runs on KM Hub servers over several minutes to an hour, so start it, tell the person it is off and running, and check on it later with km_lead_scout_status. The session can end in between.',
|
|
77
|
+
'What it will NOT do: it never emails anybody. Contacts land in the pipeline and each first message is written as a DRAFT in the Approval Queue for a person to read and approve. It refuses to run at all if the workspace has used up its monthly allowance, and it will say so instead of spending.',
|
|
78
|
+
].join(' '),
|
|
79
|
+
{
|
|
80
|
+
describe: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('Who to find, in plain English, in the person\'s own words. For example: "owners of boutique hotels in Miami that host weddings". This is the main input - prefer it over the filter lists below.'),
|
|
84
|
+
target_count: z
|
|
85
|
+
.number()
|
|
86
|
+
.int()
|
|
87
|
+
.min(1)
|
|
88
|
+
.max(100)
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('How many contacts to aim for. Default 25. More costs more, and only about one person in three turns out to have a reachable email, so a bigger number also takes longer. Capped at 100 from here.'),
|
|
91
|
+
area: z.string().optional().describe('The town, city or region, if the person named one separately from the description.'),
|
|
92
|
+
titles: z.array(z.string()).optional().describe('Job titles to look for, only if the person named specific ones. For example ["Owner", "Events Manager"].'),
|
|
93
|
+
locations: z.array(z.string()).optional().describe('Cities or regions as separate entries, if the person listed several.'),
|
|
94
|
+
industries: z.array(z.string()).optional().describe('Industries to look in, if the person named them.'),
|
|
95
|
+
keywords: z.array(z.string()).optional().describe('Extra words that should show up on the person or the business.'),
|
|
96
|
+
company_sizes: z.array(z.string()).optional().describe('Company head-count bands, only if the person asked for a size. For example ["1,10", "11,50"].'),
|
|
97
|
+
seniorities: z.array(z.string()).optional().describe('Seniority levels, only if the person asked. For example ["owner", "founder", "director"].'),
|
|
98
|
+
avoid_keywords: z
|
|
99
|
+
.array(z.string())
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Words that disqualify a lead, matched whole-word against the company name, job title and snippet. For example ["ticketing", "parking", "dean"]. This narrows a search, it cannot be the only filter.'),
|
|
102
|
+
keep_keywords: z
|
|
103
|
+
.array(z.string())
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Words that rescue a lead from an avoid word, but only for the avoid words listed in rescuable_avoid_keywords. For example ["events", "homecoming", "graduation"].'),
|
|
106
|
+
rescuable_avoid_keywords: z
|
|
107
|
+
.array(z.string())
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Which avoid_keywords a keep_keyword is allowed to override. Use for avoid words that name a DEPARTMENT rather than a job, since those also catch that department\'s event desk - who is usually the buyer. For example ["director of athletics", "chancellor"]. Leave out to make every avoid word absolute.'),
|
|
110
|
+
outreach_context: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('What the person is offering these leads, in a sentence, so the drafted first message is about something real. For example: "balloon installations for corporate galas in Austin, entrance arches and centrepieces".'),
|
|
114
|
+
vertical_flavor: z.string().optional().describe('The trade the workspace is in, if it needs to be forced. Normally leave this out and let KM Hub use the workspace setting.'),
|
|
115
|
+
deep_research: z
|
|
116
|
+
.boolean()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Read up on each business before writing to it. On by default. Turning it off makes the run cheaper and faster but the first messages far more generic.'),
|
|
119
|
+
ai_copywriting: z
|
|
120
|
+
.boolean()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('Write a first message for each contact, as a draft. On by default. Turn it off to just collect verified contacts with no message written.'),
|
|
123
|
+
confirm_spend: z
|
|
124
|
+
.boolean()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('Leave this out on the first call: you get the price back and nothing is charged. Set it to true ONLY after the person has been told the price and has agreed to it.'),
|
|
127
|
+
confirm_token: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe(
|
|
131
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
132
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
133
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
134
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
async (args) => {
|
|
138
|
+
const r = await call('POST', '/sourcing/missions', args);
|
|
139
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
140
|
+
return out(r);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
server.tool(
|
|
145
|
+
'km_list_lead_scouts',
|
|
146
|
+
'List the recent lead scouts in this workspace, newest first, with what stage each one is at. Free to call and changes nothing. Reach for it when someone asks whether anything is running, what happened to the search from yesterday, or how the last few went. If one is still going, follow up with km_lead_scout_status for its live progress. It does not start anything and does not charge anything.',
|
|
147
|
+
{
|
|
148
|
+
limit: z.number().int().min(1).max(50).optional().describe('How many to list. Default 10.'),
|
|
149
|
+
status: z
|
|
150
|
+
.enum(['queued', 'running', 'succeeded', 'failed', 'canceled'])
|
|
151
|
+
.optional()
|
|
152
|
+
.describe('Only show scouts in this state. Leave out to see them all.'),
|
|
153
|
+
},
|
|
154
|
+
async ({ limit, status }) => {
|
|
155
|
+
const r = await call('GET', `/sourcing/missions${qs({ limit: limit ?? 10, status })}`);
|
|
156
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
157
|
+
return out(r);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
server.tool(
|
|
162
|
+
'km_lead_scout_status',
|
|
163
|
+
'Check on one lead scout: what it is doing right now, how many people it has found, verified and written to so far, and its final results once it is done. Free to call and changes nothing. Reach for it after starting a scout, or whenever someone asks how the search is getting on. Report it back as a plain sentence about where it has got to, not as a list of numbers. If it says failed, the error field explains why and the person can start a fresh one. It does not restart anything and does not charge anything.',
|
|
164
|
+
{
|
|
165
|
+
mission_id: z.string().describe('The id of the scout, as returned when it was started or by km_list_lead_scouts.'),
|
|
166
|
+
},
|
|
167
|
+
async ({ mission_id }) => {
|
|
168
|
+
const id = String(mission_id || '').trim();
|
|
169
|
+
if (!id) return text('I need the id of the scout to look it up. km_list_lead_scouts will show you the recent ones.');
|
|
170
|
+
const r = await call('GET', `/sourcing/missions/${encodeURIComponent(id)}`);
|
|
171
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
172
|
+
return out(r);
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
server.tool(
|
|
177
|
+
'km_stop_lead_scout',
|
|
178
|
+
'Stop a lead scout that is queued or already running. Reach for it the moment someone says stop, cancel, that is enough, or realises the search was set up wrong. Stopping is the money-saving move and is always safe: the workspace is only charged for the lookups already made, never for the work the scout did not do. Anything it already found stays in the pipeline, and any message it already wrote stays as a draft in the Approval Queue. It cannot stop a scout that has already finished, and it will say so plainly rather than pretending. It does not delete anything.',
|
|
179
|
+
{
|
|
180
|
+
mission_id: z.string().describe('The id of the scout to stop, from km_list_lead_scouts or from when it was started.'),
|
|
181
|
+
confirm_token: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe(
|
|
185
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
186
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
187
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
188
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
189
|
+
),
|
|
190
|
+
},
|
|
191
|
+
async ({ mission_id, confirm_token }) => {
|
|
192
|
+
const id = String(mission_id || '').trim();
|
|
193
|
+
if (!id) return text('I need the id of the scout to stop it. km_list_lead_scouts will show you which ones are still running.');
|
|
194
|
+
const r = await call('POST', `/sourcing/missions/${encodeURIComponent(id)}/cancel`, { confirm_token });
|
|
195
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
196
|
+
return out(r);
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// The two small paid lookups: one company, or one address.
|
|
201
|
+
|
|
202
|
+
server.tool(
|
|
203
|
+
'km_find_company_contacts',
|
|
204
|
+
[
|
|
205
|
+
'Find the decision-makers at ONE named business and get their real email addresses. Use it when the person has a specific company in mind: a venue they want to work with, a business that just opened nearby, a lead in the pipeline with a company name but no contact.',
|
|
206
|
+
'THIS COSTS REAL MONEY, though only a small fraction of a full scout: it looks up at most five people at that one business.',
|
|
207
|
+
'How to use it: call it FIRST without confirm_spend. That call is free, changes nothing, and comes back with the price. Tell the person, get a yes, then call again with confirm_spend set to true.',
|
|
208
|
+
'For finding many people across many companies at once, do not loop this tool. Use km_start_lead_scout instead, which is built for it and cheaper per contact.',
|
|
209
|
+
'What it will NOT do: it never writes to anyone. It hands back names, titles and addresses, and nothing more. If nobody reachable turns up, nothing is charged for the empty result.',
|
|
210
|
+
].join(' '),
|
|
211
|
+
{
|
|
212
|
+
company: z.string().optional().describe('The business name, as the person said it. For example "The Grand Ballroom" or "Dazzling Balloons".'),
|
|
213
|
+
website: z.string().optional().describe('The company website, if known. Helps when the name is a common one. Either company or website is required.'),
|
|
214
|
+
location: z.string().optional().describe('Town or city, if it helps tell this business apart from others with the same name.'),
|
|
215
|
+
confirm_spend: z
|
|
216
|
+
.boolean()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe('Leave this out on the first call: you get the price back and nothing is charged. Set it to true ONLY after the person has agreed to the cost.'),
|
|
219
|
+
confirm_token: z
|
|
220
|
+
.string()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe(
|
|
223
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
224
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
225
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
226
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
227
|
+
),
|
|
228
|
+
},
|
|
229
|
+
async (args) => {
|
|
230
|
+
const r = await call('POST', '/sourcing/company-contacts', args);
|
|
231
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
232
|
+
return out(r);
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
server.tool(
|
|
237
|
+
'km_check_email_address',
|
|
238
|
+
[
|
|
239
|
+
'Check whether one email address is real and safe to write to, before anybody writes to it. Reach for it when someone reads an address off a business card or a website, when a contact in the pipeline looks doubtful, or before a message goes out to an address nobody has ever used.',
|
|
240
|
+
'This matters more than it sounds: writing to dead addresses is what gets a sender flagged as spam, and once that happens the good messages stop arriving too.',
|
|
241
|
+
'It COSTS REAL MONEY, but only a small amount for one address, and it charges nothing at all for an address that is obviously mistyped.',
|
|
242
|
+
'How to use it: call it FIRST without confirm_spend to see the price for free, then again with confirm_spend set to true. For a routine single check, telling the person the amount that came back and going ahead is usually enough.',
|
|
243
|
+
'It answers one of four ways: safe to write to, will bounce, risky, or could not tell. It also flags shared inboxes like info@ or sales@, which get far weaker replies than a named person.',
|
|
244
|
+
'What it will NOT do: it checks one address per call, it never writes to the address, and it never adds anybody to anything.',
|
|
245
|
+
].join(' '),
|
|
246
|
+
{
|
|
247
|
+
email: z.string().describe('The single email address to check.'),
|
|
248
|
+
confirm_spend: z
|
|
249
|
+
.boolean()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe('Leave this out to see the price first without being charged. Set it to true to actually run the check.'),
|
|
252
|
+
confirm_token: z
|
|
253
|
+
.string()
|
|
254
|
+
.optional()
|
|
255
|
+
.describe(
|
|
256
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
257
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
258
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
259
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
async (args) => {
|
|
263
|
+
const r = await call('POST', '/sourcing/verify-email', args);
|
|
264
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
265
|
+
return out(r);
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
}
|